From bae0ce48a85a0caaf81e7de747e0567ebd30236b Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 21 Sep 2023 17:48:53 +0200 Subject: [PATCH 1/2] BaseController: log experience improvement. use a child class logger instead of abstract class controller --- .../org/thingsboard/server/controller/BaseController.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 0af55d7c00..365f1fc379 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -21,7 +21,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; -import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; @@ -174,10 +174,11 @@ import static org.thingsboard.server.common.data.query.EntityKeyType.ENTITY_FIEL import static org.thingsboard.server.controller.UserController.YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION; import static org.thingsboard.server.dao.service.Validator.validateId; -@Slf4j @TbCoreComponent public abstract class BaseController { + private final Logger log = org.slf4j.LoggerFactory.getLogger(getClass()); + /*Swagger UI description*/ @Autowired From 91fc161c4fa6a8f1a92cb60a2bda04d9ecd3c717 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 28 Sep 2023 18:16:20 +0300 Subject: [PATCH 2/2] Implement widget types / bundles pagination and full text search. Add widget types tags. UI: Implement scroll grid component. --- .../main/data/upgrade/3.6.0/schema_update.sql | 18 + .../controller/WidgetTypeController.java | 54 ++- .../controller/WidgetsBundleController.java | 9 +- .../install/ThingsboardInstallService.java | 3 + .../constructor/WidgetTypeMsgConstructor.java | 5 + .../SystemWidgetTypesEdgeEventFetcher.java | 3 +- .../SystemWidgetsBundlesEdgeEventFetcher.java | 2 +- .../TenantWidgetTypesEdgeEventFetcher.java | 3 +- .../DefaultSystemDataLoaderService.java | 16 +- .../install/SqlDatabaseUpgradeService.java | 13 + .../server/dao/widget/WidgetTypeService.java | 9 +- .../dao/widget/WidgetsBundleService.java | 4 +- .../common/data/widget/DeprecatedFilter.java | 22 ++ .../common/data/widget/WidgetTypeDetails.java | 4 + .../common/data/widget/WidgetTypeInfo.java | 7 +- common/edge-api/src/main/proto/edge.proto | 1 + .../server/dao/model/ModelConstants.java | 5 + .../model/sql/WidgetTypeDetailsEntity.java | 8 + .../dao/model/sql/WidgetTypeInfoEntity.java | 34 +- .../dao/sql/widget/JpaWidgetTypeDao.java | 55 ++- .../dao/sql/widget/JpaWidgetsBundleDao.java | 49 ++- .../sql/widget/WidgetTypeInfoRepository.java | 134 +++++++ .../dao/sql/widget/WidgetTypeRepository.java | 31 -- .../sql/widget/WidgetsBundleRepository.java | 49 ++- .../dao/util/mapping/AbstractArrayType.java | 45 +++ .../mapping/AbstractArrayTypeDescriptor.java | 119 +++++++ .../util/mapping/ArraySqlTypeDescriptor.java | 86 +++++ .../server/dao/util/mapping/ArrayUtil.java | 335 ++++++++++++++++++ .../mapping/ParameterizedParameterType.java | 64 ++++ .../dao/util/mapping/StringArrayType.java | 41 +++ .../mapping/StringArrayTypeDescriptor.java | 31 ++ .../server/dao/widget/WidgetTypeDao.java | 16 +- .../dao/widget/WidgetTypeServiceImpl.java | 32 +- .../server/dao/widget/WidgetsBundleDao.java | 4 +- .../dao/widget/WidgetsBundleServiceImpl.java | 16 +- .../main/resources/sql/schema-entities.sql | 1 + .../sql/schema-views-and-functions.sql | 6 + .../dao/service/WidgetsBundleServiceTest.java | 8 +- .../dao/sql/widget/JpaWidgetTypeDaoTest.java | 67 +++- .../sql/widget/JpaWidgetsBundleDaoTest.java | 137 ++++++- ui-ngx/src/app/core/http/entity.service.ts | 2 +- ui-ngx/src/app/core/http/widget.service.ts | 61 ++-- .../dashboard-page.component.html | 43 ++- .../dashboard-page.component.ts | 13 +- .../dashboard-widget-select.component.html | 128 ++++--- .../dashboard-widget-select.component.scss | 136 +++---- .../dashboard-widget-select.component.ts | 309 ++++++++-------- .../grid/scroll-grid.component.html | 53 +++ .../grid/scroll-grid.component.scss | 32 ++ .../components/grid/scroll-grid.component.ts | 103 ++++++ .../home/components/home-components.module.ts | 7 +- .../datasource/scroll-grid-datasource.ts | 250 +++++++++++++ .../home/models/widget-component.models.ts | 3 + .../pages/widget/widget-editor.component.html | 5 + .../widget/widget-library-routing.module.ts | 2 +- .../pages/widget/widget-type.component.html | 4 + .../pages/widget/widget-type.component.ts | 2 + .../widgets-bundle-widgets.component.ts | 2 +- ui-ngx/src/app/shared/models/widget.models.ts | 8 + .../assets/locale/locale.constant-en_US.json | 1 + 60 files changed, 2228 insertions(+), 482 deletions(-) create mode 100644 application/src/main/data/upgrade/3.6.0/schema_update.sql create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/widget/DeprecatedFilter.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayType.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayTypeDescriptor.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArraySqlTypeDescriptor.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArrayUtil.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/mapping/ParameterizedParameterType.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayType.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayTypeDescriptor.java create mode 100644 ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html create mode 100644 ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts create mode 100644 ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts diff --git a/application/src/main/data/upgrade/3.6.0/schema_update.sql b/application/src/main/data/upgrade/3.6.0/schema_update.sql new file mode 100644 index 0000000000..dd07a47dac --- /dev/null +++ b/application/src/main/data/upgrade/3.6.0/schema_update.sql @@ -0,0 +1,18 @@ +-- +-- 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. +-- + +ALTER TABLE widget_type + ADD COLUMN IF NOT EXISTS tags text[]; \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java index 4aeaeb2210..f90a196edf 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java +++ b/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 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 getBundleWidgetTypesInfos( + public PageData 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 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)", diff --git a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java b/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java index 382546dc96..b07467b94b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java +++ b/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)); } } diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 208da7d36c..3829b3cf45 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -262,6 +262,9 @@ public class ThingsboardInstallService { databaseEntitiesUpgradeService.upgradeDatabase("3.5.1"); dataUpdateService.updateData("3.5.1"); systemDataLoaderService.updateDefaultNotificationConfigs(); + case "3.6.0": + log.info("Upgrading ThingsBoard from version 3.6.0 to 3.6.1 ..."); + databaseEntitiesUpgradeService.upgradeDatabase("3.6.0"); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java index af574b4b75..e6f04261fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java +++ b/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(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java index de4218fc53..4bb4d1fb06 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java +++ b/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 findWidgetTypes(TenantId tenantId, PageLink pageLink) { - return widgetTypeService.findSystemWidgetTypesByPageLink(tenantId, false, pageLink); + return widgetTypeService.findSystemWidgetTypesByPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java index 87cffcee9f..d1ee0723dd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java +++ b/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 findWidgetsBundles(TenantId tenantId, PageLink pageLink) { - return widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, pageLink); + return widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java index 24f0527c20..85e23427d3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java +++ b/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 findWidgetTypes(TenantId tenantId, PageLink pageLink) { - return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(tenantId, false, pageLink); + return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 4bb3645f20..29dcd601a1 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/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 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()); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 307e2e5f3a..908fc75ec6 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -777,6 +777,19 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService log.error("Failed updating schema!!!", e); } break; + case "3.6.0": + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + log.info("Updating schema ..."); + if (isOldSchema(conn, 3006000)) { + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.6.0", SCHEMA_UPDATE_SQL); + loadSql(schemaUpdateFile, conn); + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3006001;"); + } + log.info("Schema updated."); + } catch (Exception e) { + log.error("Failed updating schema!!!", e); + } + break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java index edc6bb21da..d8df5d61a3 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java +++ b/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 findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); + PageData findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); - PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); + PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); - PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); + PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); List findWidgetTypesByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId); List findWidgetTypesDetailsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId); - List findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId); + PageData findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); List findWidgetFqnsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java index e350846881..15fd4838dd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java +++ b/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 findSystemWidgetsBundlesByPageLink(TenantId tenantId, PageLink pageLink); + PageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); List findSystemWidgetsBundles(TenantId tenantId); PageData findTenantWidgetsBundlesByTenantId(TenantId tenantId, PageLink pageLink); - PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink); + PageData findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink); List findAllTenantWidgetsBundlesByTenantId(TenantId tenantId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/DeprecatedFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/DeprecatedFilter.java new file mode 100644 index 0000000000..8ceb0d48a9 --- /dev/null +++ b/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 +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java index cd6f13c6d0..424e0bc6bc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java +++ b/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(); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java index 0e8bce0a19..3387fb13b6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java +++ b/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 { diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index b316d5b3c4..bc0170b24b 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/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 { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 0e8ea2db15..d27a5365ef 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/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. */ diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java index be934e9c8f..99555393c6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java +++ b/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 { @@ -45,6 +47,10 @@ public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity { + @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; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index 0548ad814d..1d446be1b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/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 findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, PageLink pageLink) { + public PageData findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List 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 findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) { + public PageData findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List 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 findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) { + public PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List 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 findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId) { - return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByWidgetsBundleId(widgetsBundleId)); + public PageData findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List 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 diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java index f3a476d8eb..e9c972c7a3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java @@ -62,13 +62,22 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao findSystemWidgetsBundles(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData( - widgetsBundleRepository - .findSystemWidgetsBundles( - NULL_UUID, - Objects.toString(pageLink.getTextSearch(), ""), - DaoUtil.toPageable(pageLink))); + public PageData 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 findAllTenantWidgetsBundlesByTenantId(UUID tenantId, PageLink pageLink) { - return DaoUtil.toPageData( - widgetsBundleRepository - .findAllTenantWidgetsBundlesByTenantId( - tenantId, - NULL_UUID, - Objects.toString(pageLink.getTextSearch(), ""), - DaoUtil.toPageable(pageLink))); + public PageData 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 diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java new file mode 100644 index 0000000000..27f4ef918f --- /dev/null +++ b/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 { + + @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 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 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 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 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 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 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 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 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 widgetTypes, + Pageable pageable); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java index 41fa293dd5..3c95cdb1f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java +++ b/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 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 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 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 findTenantWidgetTypeDetailsByTenantId(@Param("tenantId") UUID tenantId, @@ -78,10 +51,6 @@ public interface WidgetTypeRepository extends JpaRepository 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 findWidgetTypesInfosByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId); @Query("SELECT wtd.fqn FROM WidgetTypeDetailsEntity wtd, WidgetsBundleWidgetEntity wbw " + "WHERE wbw.widgetsBundleId = :widgetsBundleId " + diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java index 7a6ba68a36..25053bbbe6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java @@ -33,11 +33,33 @@ public interface WidgetsBundleRepository extends JpaRepository 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 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 findTenantWidgetsBundlesByTenantId(@Param("tenantId") UUID tenantId, @@ -51,6 +73,29 @@ public interface WidgetsBundleRepository extends JpaRepository 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") diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayType.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayType.java new file mode 100644 index 0000000000..a194b76aa5 --- /dev/null +++ b/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 + extends AbstractSingleColumnStandardBasicType + implements DynamicParameterizedType { + + public static final String SQL_ARRAY_TYPE = "sql_array_type"; + + public AbstractArrayType(AbstractArrayTypeDescriptor arrayTypeDescriptor) { + super( + ArraySqlTypeDescriptor.INSTANCE, + arrayTypeDescriptor + ); + } + + @Override + protected boolean registerUnderJavaType() { + return true; + } + + @Override + public void setParameterValues(Properties parameters) { + ((AbstractArrayTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayTypeDescriptor.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayTypeDescriptor.java new file mode 100644 index 0000000000..91e426567c --- /dev/null +++ b/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 + extends AbstractTypeDescriptor implements DynamicParameterizedType { + + private Class arrayObjectClass; + + private String sqlArrayType; + + public AbstractArrayTypeDescriptor(Class arrayObjectClass) { + this(arrayObjectClass, (MutabilityPlan) new MutableMutabilityPlan() { + @Override + protected T deepCopyNotNull(Object value) { + return ArrayUtil.deepCopy(value); + } + }); + } + + protected AbstractArrayTypeDescriptor(Class arrayObjectClass, MutabilityPlan mutableMutabilityPlan) { + super(arrayObjectClass, mutableMutabilityPlan); + this.arrayObjectClass = arrayObjectClass; + } + + public Class getArrayObjectClass() { + return arrayObjectClass; + } + + public void setArrayObjectClass(Class 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 unwrap(T value, Class type, WrapperOptions options) { + return (X) ArrayUtil.wrapArray(value); + } + + @Override + public 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; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArraySqlTypeDescriptor.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArraySqlTypeDescriptor.java new file mode 100644 index 0000000000..4baa150445 --- /dev/null +++ b/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 ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { + return new BasicBinder(javaTypeDescriptor, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + AbstractArrayTypeDescriptor abstractArrayTypeDescriptor = (AbstractArrayTypeDescriptor) 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 ValueExtractor getExtractor(final JavaTypeDescriptor javaTypeDescriptor) { + return new BasicExtractor(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); + } + }; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArrayUtil.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArrayUtil.java new file mode 100644 index 0000000000..eb940c1912 --- /dev/null +++ b/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 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 unwrapArray(Object[] originalArray, Class 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 fromString(String string, Class 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 Class toArrayClass(Class 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) array.getClass(); + } + } + + public static List asList(T[] array) { + List list = new ArrayList(array.length); + for (int i = 0; i < array.length; i++) { + list.add(i, array[i]); + } + return list; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/ParameterizedParameterType.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/ParameterizedParameterType.java new file mode 100644 index 0000000000..53faf75720 --- /dev/null +++ b/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(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayType.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayType.java new file mode 100644 index 0000000000..cd2ee52a35 --- /dev/null +++ b/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 { + + 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"; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayTypeDescriptor.java b/dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayTypeDescriptor.java new file mode 100644 index 0000000000..8846d0a5e2 --- /dev/null +++ b/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 { + + public StringArrayTypeDescriptor() { + super(String[].class); + } + + @Override + protected String getSqlArrayType() { + String sqlArrayType = super.getSqlArrayType(); + return sqlArrayType != null ? sqlArrayType : "text"; + } +} + diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java index d9d19a9617..96d93a34f1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java +++ b/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, ExportableEntityD boolean existsByTenantIdAndId(TenantId tenantId, UUID widgetTypeId); - PageData findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, PageLink pageLink); + PageData findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); - PageData findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink); + PageData findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); - PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink); + PageData findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); /** * Find widget types by widgetsBundleId. @@ -77,14 +78,7 @@ public interface WidgetTypeDao extends Dao, ExportableEntityD */ List 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 findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId); + PageData findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List widgetTypes, PageLink pageLink); List findWidgetFqnsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java index 4bd0b62f4b..2e347f2b62 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java +++ b/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 findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { - log.trace("Executing findSystemWidgetTypesByPageLink, fullSearch [{}] pageLink [{}]", fullSearch, pageLink); + public PageData findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List 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 findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { - log.trace("Executing findAllTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink); + public PageData findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List 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 findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) { - log.trace("Executing findTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink); + public PageData findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List 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 findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId) { - log.trace("Executing findWidgetTypesInfosByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}]", tenantId, widgetsBundleId); + public PageData findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId, boolean fullSearch, + DeprecatedFilter deprecatedFilter, List 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 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 diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java index e4b1762426..b5486b61b1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java @@ -54,7 +54,7 @@ public interface WidgetsBundleDao extends Dao, ExportableEntityDa * @param pageLink the page link * @return the list of widgets bundles objects */ - PageData findSystemWidgetsBundles(TenantId tenantId, PageLink pageLink); + PageData 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, ExportableEntityDa * @param pageLink the page link * @return the list of widgets bundles objects */ - PageData findAllTenantWidgetsBundlesByTenantId(UUID tenantId, PageLink pageLink); + PageData findAllTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java index 4f094ca62f..998cbb6e31 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java @@ -104,10 +104,10 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService { } @Override - public PageData findSystemWidgetsBundlesByPageLink(TenantId tenantId, PageLink pageLink) { - log.trace("Executing findSystemWidgetsBundles, pageLink [{}]", pageLink); + public PageData 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 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 findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink) { - log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink); + public PageData 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 pageData; do { - pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink); + pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink); widgetsBundles.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 4df73de9d6..b1ae32704e 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/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) diff --git a/dao/src/main/resources/sql/schema-views-and-functions.sql b/dao/src/main/resources/sql/schema-views-and-functions.sql index 3aa797d7b6..1aa5647bde 100644 --- a/dao/src/main/resources/sql/schema-views-and-functions.sql +++ b/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; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java index 0f5c90b4a8..da594bb9c7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java +++ b/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 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 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(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java index 8d289c6e1a..420887c844 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java +++ b/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 widgetTypeList; + List 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 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 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 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); diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java index e1758c4660..424bda88f7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java +++ b/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 widgetsBundles; + List 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 widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, pageLink); + PageData widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, false, pageLink); assertEquals(10, widgetsBundles1.getData().size()); // Get next page pageLink = pageLink.nextPageLink(); - PageData widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, pageLink); + PageData 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 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 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 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 widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink); + PageData widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink); assertEquals(30, widgetsBundles1.getData().size()); pageLink = pageLink.nextPageLink(); - PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink); + PageData widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink); assertEquals(30, widgetsBundles2.getData().size()); pageLink = pageLink.nextPageLink(); - PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink); + PageData widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink); assertEquals(10, widgetsBundles3.getData().size()); pageLink = pageLink.nextPageLink(); - PageData widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink); + PageData 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 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 widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink); + assertEquals(0, widgetsBundles2.getData().size()); + + PageData 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 widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink); + assertEquals(1, widgetsBundles4.getData().size()); + assertEquals(widgetsBundle1, widgetsBundles4.getData().get(0)); + + PageData 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 widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId, textPageLink); + PageData 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; + } } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 12f6300b93..69ca114001 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/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'; diff --git a/ui-ngx/src/app/core/http/widget.service.ts b/ui-ngx/src/app/core/http/widget.service.ts index 6affb01efe..9dcda4ef8a 100644 --- a/ui-ngx/src/app/core/http/widget.service.ts +++ b/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; private tenantWidgetsBundles: Array; - private widgetTypeInfosCache = new Map>(); - private widgetsInfoInMemoryCache = new Map(); private loadWidgetsBundleCacheSubject: ReplaySubject; @@ -86,8 +85,8 @@ export class WidgetService { ); } - public getWidgetBundles(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/widgetsBundles${pageLink.toQuery()}`, + public getWidgetBundles(pageLink: PageLink, fullSearch = false, config?: RequestConfig): Observable> { + return this.http.get>(`/api/widgetsBundles${pageLink.toQuery()}&fullSearch=${fullSearch}`, defaultHttpOptionsFromConfig(config)); } @@ -109,21 +108,13 @@ export class WidgetService { public updateWidgetsBundleWidgetTypes(widgetsBundleId: string, widgetTypeIds: Array, config?: RequestConfig): Observable { return this.http.post(`/api/widgetsBundle/${widgetsBundleId}/widgetTypes`, widgetTypeIds, - defaultHttpOptionsFromConfig(config)).pipe( - tap(() => { - this.widgetTypeInfosCache.delete(widgetsBundleId); - }) - ); + defaultHttpOptionsFromConfig(config)); } public updateWidgetsBundleWidgetFqns(widgetsBundleId: string, widgetTypeFqns: Array, config?: RequestConfig): Observable { return this.http.post(`/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> { - if (this.widgetTypeInfosCache.has(widgetsBundleId)) { - return of(this.widgetTypeInfosCache.get(widgetsBundleId)); - } else { - return this.http.get>(`/api/widgetTypesInfos?widgetsBundleId=${widgetsBundleId}`, - defaultHttpOptionsFromConfig(config)).pipe( - tap((res) => this.widgetTypeInfosCache.set(widgetsBundleId, res) ) - ); + public getBundleWidgetTypeInfosList(widgetsBundleId: string, + config?: RequestConfig): Observable> { + 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 = null, + config?: RequestConfig): Observable> { + + let url = + `/api/widgetTypesInfos${pageLink.toQuery()}&widgetsBundleId=${widgetsBundleId}` + + `&fullSearch=${fullSearch}&deprecatedFilter=${deprecatedFilter}`; + if (widgetTypes && widgetTypes.length) { + url += `&widgetTypeList=${widgetTypes.join(',')}`; } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } public getWidgetType(fullFqn: string, config?: RequestConfig): Observable { @@ -224,10 +226,14 @@ export class WidgetService { } public getWidgetTypes(pageLink: PageLink, tenantOnly = false, - fullSearch = false, config?: RequestConfig): Observable> { - return this.http.get>( - `/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}`, - defaultHttpOptionsFromConfig(config)); + fullSearch = false, deprecatedFilter = DeprecatedFilter.ALL, widgetTypes: Array = null, + config?: RequestConfig): Observable> { + let url = + `/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}&deprecatedFilter=${deprecatedFilter}`; + if (widgetTypes && widgetTypes.length) { + url += `&widgetTypeList=${widgetTypes.join(',')}`; + } + return this.http.get>(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(); } } diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html index 9b5a742ecf..166e359799 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html @@ -386,7 +386,7 @@ -
+ (closeSearch)="dashboardWidgetSelectComponent.search = ''"> +
-
+
+ [(ngModel)]="dashboardWidgetSelectComponent.search" + placeholder="{{ ((!dashboardWidgetSelectComponent.widgetsBundle && dashboardWidgetSelectComponent.selectWidgetMode === 'bundles') + ? 'widgets-bundle.search' : 'widget.search') | translate }}">
- - {{ 'widgets-bundle.widgets-bundles' | translate }} {{ 'widget.all-widgets' | translate }} + - {{ 'widget.all' | translate }} - {{ 'widget.actual' | translate }} - {{ 'widget.deprecated' | translate }} + [(ngModel)]="dashboardWidgetSelectComponent.deprecatedFilter"> + {{ 'widget.all' | translate }} + {{ 'widget.actual' | translate }} + {{ 'widget.deprecated' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index a9ce196203..af76812fa1 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/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(); diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html index 44882ab889..a0c7bec2e0 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html @@ -15,79 +15,105 @@ limitations under the License. --> -
+
-
- -
+ + +
- + - -
-
- -
- {{ widget.title }} -
-
- {{widget.title}}
widget.deprecated
- {{ 'widget.' + widget.type | translate }} - - {{ widget.description }} - -
-
-
-
-
- -
+ + + + + +
+ {{ item.title }} +
+
+ {{item.name}}
widget.deprecated
+ {{ 'widget.' + item.widgetType | translate }} + + {{ item.description }} + +
+
+
+ +
{{ 'widget.loading-widgets' | translate }} - -
-
- - +
+
+ + {{(selectWidgetMode === 'bundles' ? 'widgets-bundle.empty' : 'widget.no-widgets-text') | translate}} + + -
-
- -
- {{ widgetsBundle.title }} -
-
- {{ widgetsBundle.title }} - widgets-bundle.system - - {{ widgetsBundle.description }} - -
-
-
-
+ + + + +
+ {{ item.title }} +
+
+ {{ item.title }} + widgets-bundle.system + + {{ item.description }} + +
+
+
-
{{ 'widgets-bundle.loading-widgets-bundles' | translate }}
- + + widgets-bundle.no-widgets-bundles-text -
+ + +
+
+
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss index 3a290e53e5..3641638247 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss +++ b/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%; + .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%) + } + + &.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-content { + height: 100%; + margin: 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-container { + text-align: center; + margin: auto 0; + } + + .preview { max-width: 100%; + max-height: 100%; + object-fit: contain; + } - &: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%) - } - } + .mat-mdc-card-title { + font-size: 20px; + line-height: normal; + margin-bottom: 8px; - .mat-mdc-card { - cursor: pointer; - transition: box-shadow 0.2s; - padding: 16px; - - .preview-container { - text-align: center; - margin: auto 0; - } - - .preview { - max-width: 100%; - max-height: 100%; - object-fit: contain; - } - - .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); - } - } - - .mat-mdc-card-subtitle { - margin-bottom: 16px; - margin-top: -4px; - line-height: normal; - font-weight: normal; - } - - .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%; + } +} diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts index 8a359de66c..7f661df575 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts +++ b/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(''); + private searchSubject = new BehaviorSubject(''); + private search$ = this.searchSubject.asObservable().pipe( + debounceTime(150)); + private filterWidgetTypes$ = new BehaviorSubject>(null); - private widgetsListMode$ = new BehaviorSubject('actual'); + private deprecatedFilter$ = new BehaviorSubject(DeprecatedFilter.ACTUAL); private selectWidgetMode$ = new BehaviorSubject('bundles'); - private widgetsInfo: Observable>; - private widgetsBundleValue: WidgetsBundle; + private widgetsBundle$ = new BehaviorSubject(null); + widgetTypes = new Set(); hasDeprecated = false; - allWidgets$: Observable>; - widgets$: Observable>; - loadingWidgetsSubject: BehaviorSubject = new BehaviorSubject(false); - loadingWidgets$ = this.loadingWidgetsSubject.pipe( - share() - ); - widgetsBundles$: Observable>; - loadingWidgetBundlesSubject: BehaviorSubject = 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(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 widgetsListMode(): widgetsListMode { - return this.widgetsListMode$.value; + 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(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 widgetsBundle(): WidgetsBundle { + return this.widgetsBundle$.value; } @Output() widgetSelected: EventEmitter = new EventEmitter(); - @Output() - widgetsBundleSelected: EventEmitter = new EventEmitter(); + columns: ScrollGridColumns = { + columns: 1, + breakpoints: { + 'screen and (min-width: 2000px)': 3, + 'screen and (min-width: 600px)': 2 + } + }; + + widgetBundlesFetchFunction: GridEntitiesFetchFunction; + allWidgetsFetchFunction: GridEntitiesFetchFunction; + widgetsFetchFunction: GridEntitiesFetchFunction; + + 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 }) - ); - this.widgets$ = combineLatest([this.search$.asObservable(), this.filterWidgetTypes$.asObservable(), this.widgetsListMode$]).pipe( + + combineLatest({search: this.search$, 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) => { + this.allWidgetsFilter = filter; + this.cd.markForCheck(); + } + ); + + 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)), + skip(1) + ).subscribe( + (filter) => { + if (filter.widgetsBundleId) { + this.widgetsFilter = filter; + this.cd.markForCheck(); + } + } ); } ngOnInit(): void { } - private getWidgets(): Observable> { - 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(); - 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> { - 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> { - 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> { - 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> { - 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> { - 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 { diff --git a/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html b/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html new file mode 100644 index 0000000000..5340293a12 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html @@ -0,0 +1,53 @@ + + + +
+
+ + + + + + +
+
+
+
+ + + + + + + + + + + + +
+ +
+
+ +
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.scss b/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.scss new file mode 100644 index 0000000000..c7a6e2d4b7 --- /dev/null +++ b/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; + } +} diff --git a/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts b/ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts new file mode 100644 index 0000000000..3ca42bba07 --- /dev/null +++ b/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 implements OnInit, AfterViewInit, OnChanges { + + @ViewChild('viewport') + viewport: CdkVirtualScrollViewport; + + @Input() + columns: ScrollGridColumns = {columns: 1}; + + @Input() + fetchFunction: GridEntitiesFetchFunction; + + @Input() + filter: F; + + @Input() + itemSize = 200; + + @Input() + gap = 12; + + @Input() + itemCard: TemplateRef<{item: T}>; + + @Input() + loadingCell: TemplateRef; + + @Input() + dataLoading: TemplateRef; + + @Input() + noData: TemplateRef; + + dataSource: ScrollGridDatasource; + + constructor(private breakpointObserver: BreakpointObserver, + private renderer: Renderer2) { + } + + ngOnInit(): void { + this.dataSource = new ScrollGridDatasource(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); + } +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 735ef9742b..46e9f15275 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/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, diff --git a/ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts b/ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts new file mode 100644 index 0000000000..b3568b4bc5 --- /dev/null +++ b/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 = (pageSize: number, page: number, filter: F) => Observable>; + +export type GridCellType = 'emptyCell' | 'loadingCell'; + +export interface ScrollGridColumns { + columns: number; + breakpoints?: {[breakpoint: string]: number}; +} + +export class ScrollGridDatasource extends DataSource<(T | GridCellType)[]> { + + public initialDataLoading = true; + + private _data: T[] = []; + private _rows: (T | GridCellType)[][] = Array.from({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, + 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({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({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())) + ).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({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; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 214a78d022..61b4713cc4 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/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; componentModuleRef?: NgModuleRef; } @@ -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 }; }; diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html index f19edc35c7..f3410f9893 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html @@ -253,6 +253,11 @@ rows="2" maxlength="255"> {{descriptionInput.value?.length || 0}}/255 + + diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts index 4ac54e7a68..4c3c933f82 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts @@ -58,7 +58,7 @@ export class WidgetsBundleWidgetsResolver implements Resolve> { const widgetsBundleId = route.params.widgetsBundleId; - return this.widgetsService.getBundleWidgetTypeInfos(widgetsBundleId); + return this.widgetsService.getBundleWidgetTypeInfosList(widgetsBundleId); } } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html index b7c23c415d..19309e8660 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html @@ -58,6 +58,10 @@ {{descriptionInput.value?.length || 0}}/1024 + + {{ 'widget.deprecated' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts index 5418557631..e6354945d7 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts +++ b/ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts @@ -52,6 +52,7 @@ export class WidgetTypeComponent extends EntityComponent { 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 { name: entity.name, image: entity.image, description: entity.description, + tags: entity.tags, deprecated: entity.deprecated }); } diff --git a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts b/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts index f368650aa5..58403ad9b2 100644 --- a/ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts +++ b/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; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index a08d8837f5..d10bbc433d 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/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 { image: string; description: string; + tags: string[]; +} + +export enum DeprecatedFilter { + ALL = 'ALL', + ACTUAL = 'ACTUAL', + DEPRECATED = 'DEPRECATED' } export enum LegendDirection { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 0867f21114..536b4fb5e5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/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",