Merge with master

This commit is contained in:
Andrii Shvaika 2023-10-03 18:49:57 +03:00
commit 0360e655ae
55 changed files with 1485 additions and 485 deletions

View File

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

View File

@ -24,7 +24,7 @@ import jakarta.mail.MessagingException;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.ConstraintViolation;
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

View File

@ -29,6 +29,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;
@ -37,6 +38,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;
@ -48,6 +50,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,9 @@ 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_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";
@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 +172,22 @@ public class WidgetTypeController extends AutoCommitController {
@Parameter(description = TENANT_ONLY_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean tenantOnly,
@Parameter(description = FULL_SEARCH_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean fullSearch) throws ThingsboardException {
@RequestParam(required = false) Boolean fullSearch,
@Parameter(description = DEPRECATED_FILTER_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"ALL", "ACTUAL", "DEPRECATED"}))
@RequestParam(required = false) String deprecatedFilter,
@Parameter(description = WIDGET_TYPE_ARRAY_DESCRIPTION, schema = @Schema(allowableValues = {"timeseries", "latest", "control", "alarm", "static"}))
@RequestParam(required = false) String[] widgetTypeList) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
List<String> widgetTypes = widgetTypeList != null ? Arrays.asList(widgetTypeList) : Collections.emptyList();
boolean fullSearchBool = fullSearch != null && fullSearch;
DeprecatedFilter widgetTypeDeprecatedFilter = StringUtils.isNotEmpty(deprecatedFilter) ? DeprecatedFilter.valueOf(deprecatedFilter) : DeprecatedFilter.ALL;
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink));
return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
} else {
if (tenantOnly != null && tenantOnly) {
return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink));
return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
} else {
return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink));
return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
}
}
}
@ -272,19 +285,40 @@ public class WidgetTypeController extends AutoCommitController {
tenantId = getCurrentUser().getTenantId();
}
WidgetsBundle widgetsBundle = checkNotNull(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(tenantId, bundleAlias));
return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundle.getId()));
return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundle.getId(), false, DeprecatedFilter.ALL,
null, new PageLink(1024))).getData();
}
@ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfos)",
notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId"}, method = RequestMethod.GET)
@RequestMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId", "pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public List<WidgetTypeInfo> getBundleWidgetTypesInfos(
public PageData<WidgetTypeInfo> getBundleWidgetTypesInfos(
@Parameter(description = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@RequestParam("widgetsBundleId") String strWidgetsBundleId,
@Parameter(description = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@Parameter(description = WIDGET_TYPE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "name", "deprecated", "tenantId"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder,
@Parameter(description = FULL_SEARCH_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean fullSearch,
@Parameter(description = DEPRECATED_FILTER_PARAM_DESCRIPTION, schema = @Schema(allowableValues = {"ALL", "ACTUAL", "DEPRECATED"}))
@RequestParam(required = false) String deprecatedFilter,
@Parameter(description = WIDGET_TYPE_ARRAY_DESCRIPTION, schema = @Schema(allowableValues = {"timeseries", "latest", "control", "alarm", "static"}))
@RequestParam(required = false) String[] widgetTypeList) throws ThingsboardException {
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundleId));
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
List<String> widgetTypes = widgetTypeList != null ? Arrays.asList(widgetTypeList) : Collections.emptyList();
DeprecatedFilter widgetTypeDeprecatedFilter = StringUtils.isNotEmpty(deprecatedFilter) ? DeprecatedFilter.valueOf(deprecatedFilter) : DeprecatedFilter.ALL;
return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundleId, fullSearch != null && fullSearch,
widgetTypeDeprecatedFilter, widgetTypes, pageLink));
}
@ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)",

View File

@ -67,6 +67,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)
@ -182,13 +183,15 @@ public class WidgetsBundleController extends BaseController {
@Parameter(description = SORT_PROPERTY_DESCRIPTION, schema = @Schema(allowableValues = {"createdTime", "title", "tenantId"}))
@RequestParam(required = false) String sortProperty,
@Parameter(description = SORT_ORDER_DESCRIPTION, schema = @Schema(allowableValues = {"ASC", "DESC"}))
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
@RequestParam(required = false) String sortOrder,
@Parameter(description = 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));
}
}

View File

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

View File

@ -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();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}

View File

@ -38,6 +38,9 @@ public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantI
@Length(fieldName = "description", max = 1024)
@Schema(description = "Description of the widget")
private String description;
@NoXss
@Schema(description = "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();
}
}

View File

@ -29,6 +29,9 @@ public class WidgetTypeInfo extends BaseWidgetType {
@Schema(description = "Description of the widget type", accessMode = Schema.AccessMode.READ_ONLY)
private String description;
@NoXss
@Schema(description = "Tags of the widget type", accessMode = Schema.AccessMode.READ_ONLY)
private String[] tags;
@NoXss
@Schema(description = "Type of the widget (timeseries, latest, control, alarm or static)", accessMode = Schema.AccessMode.READ_ONLY)
private String widgetType;
@ -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 {

View File

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

View File

@ -236,6 +236,10 @@
<groupId>org.thingsboard.rule-engine</groupId>
<artifactId>rule-engine-api</artifactId>
</dependency>
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-62</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

View File

@ -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.
*/

View File

@ -16,12 +16,14 @@
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import io.hypersistence.utils.hibernate.type.array.StringArrayType;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
@ -42,6 +44,10 @@ public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity<WidgetType
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTION_PROPERTY)
private String description;
@Type(StringArrayType.class)
@Column(name = ModelConstants.WIDGET_TYPE_TAGS_PROPERTY, columnDefinition = "text[]")
private String[] tags;
@Convert(converter = JsonConverter.class)
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY)
private JsonNode descriptor;
@ -57,6 +63,7 @@ public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity<WidgetType
super(widgetTypeDetails);
this.image = widgetTypeDetails.getImage();
this.description = widgetTypeDetails.getDescription();
this.tags = widgetTypeDetails.getTags();
this.descriptor = widgetTypeDetails.getDescriptor();
if (widgetTypeDetails.getExternalId() != null) {
this.externalId = widgetTypeDetails.getExternalId().getId();
@ -69,6 +76,7 @@ public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity<WidgetType
WidgetTypeDetails widgetTypeDetails = new WidgetTypeDetails(baseWidgetType);
widgetTypeDetails.setImage(image);
widgetTypeDetails.setDescription(description);
widgetTypeDetails.setTags(tags);
widgetTypeDetails.setDescriptor(descriptor);
if (externalId != null) {
widgetTypeDetails.setExternalId(new WidgetTypeId(externalId));

View File

@ -15,40 +15,50 @@
*/
package org.thingsboard.server.dao.model.sql;
import io.hypersistence.utils.hibernate.type.array.StringArrayType;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.Type;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.model.ModelConstants;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Immutable
@Table(name = ModelConstants.WIDGET_TYPE_INFO_VIEW_TABLE_NAME)
public final class WidgetTypeInfoEntity extends AbstractWidgetTypeEntity<WidgetTypeInfo> {
@Column(name = ModelConstants.WIDGET_TYPE_IMAGE_PROPERTY)
private String image;
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTION_PROPERTY)
private String description;
@Type(StringArrayType.class)
@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;
}

View File

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

View File

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

View File

@ -0,0 +1,134 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.widget;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity;
import java.util.List;
import java.util.UUID;
public interface WidgetTypeInfoRepository extends JpaRepository<WidgetTypeInfoEntity, UUID> {
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti WHERE wti.tenant_id = :systemTenantId " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))",
// "OR to_tsvector(lower(array_to_string(wti.tags, ' '))) @@ to_tsquery(lower(:searchText)))))",
countQuery = "SELECT count(*) FROM widget_type_info_view wti WHERE wti.tenant_id = :systemTenantId " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))"
)
Page<WidgetTypeInfoEntity> findSystemWidgetTypes(@Param("systemTenantId") UUID systemTenantId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled,
@Param("deprecatedFilter") boolean deprecatedFilter,
@Param("widgetTypesEmpty") boolean widgetTypesEmpty,
@Param("widgetTypes") List<String> widgetTypes,
Pageable pageable);
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti WHERE wti.tenant_id IN (:tenantId, :nullTenantId) " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))",
countQuery = "SELECT count(*) FROM widget_type_info_view wti WHERE wti.tenant_id IN (:tenantId, :nullTenantId) " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))"
)
Page<WidgetTypeInfoEntity> findAllTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId,
@Param("nullTenantId") UUID nullTenantId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled,
@Param("deprecatedFilter") boolean deprecatedFilter,
@Param("widgetTypesEmpty") boolean widgetTypesEmpty,
@Param("widgetTypes") List<String> widgetTypes,
Pageable pageable);
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti WHERE wti.tenant_id = :tenantId " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))",
countQuery = "SELECT count(*) FROM widget_type_info_view wti WHERE wti.tenant_id = :tenantId " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))"
)
Page<WidgetTypeInfoEntity> findTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled,
@Param("deprecatedFilter") boolean deprecatedFilter,
@Param("widgetTypesEmpty") boolean widgetTypesEmpty,
@Param("widgetTypes") List<String> widgetTypes,
Pageable pageable);
@Query("SELECT wti FROM WidgetTypeInfoEntity wti, WidgetsBundleWidgetEntity wbw " +
"WHERE wbw.widgetsBundleId = :widgetsBundleId " +
"AND wbw.widgetTypeId = wti.id ORDER BY wbw.widgetTypeOrder")
List<WidgetTypeInfoEntity> findWidgetTypesInfosByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId);
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti, widgets_bundle_widget wbw " +
"WHERE wbw.widgets_bundle_id = :widgetsBundleId " +
"AND wbw.widget_type_id = wti.id " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' ')))) " +
"ORDER BY wbw.widget_type_order",
countQuery = "SELECT count(*) FROM widget_type_info_view wti, widgets_bundle_widget wbw " +
"WHERE wbw.widgets_bundle_id = :widgetsBundleId " +
"AND wbw.widget_type_id = wti.id " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))"
)
Page<WidgetTypeInfoEntity> findWidgetTypesInfosByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled,
@Param("deprecatedFilter") boolean deprecatedFilter,
@Param("widgetTypesEmpty") boolean widgetTypesEmpty,
@Param("widgetTypes") List<String> widgetTypes,
Pageable pageable);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -125,6 +125,7 @@
<wire-schema.version>3.4.0</wire-schema.version>
<twilio.version>9.6.1</twilio.version>
<hibernate-validator.version>8.0.0.Final</hibernate-validator.version>
<hypersistence-utils.version>3.5.2</hypersistence-utils.version>
<jakarta.el.version>4.0.2</jakarta.el.version>
<jakarta.validation-api.version>3.0.2</jakarta.validation-api.version>
<antisamy.version>1.7.3</antisamy.version>
@ -1968,6 +1969,11 @@
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-62</artifactId>
<version>${hypersistence-utils.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>

View File

@ -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';

View File

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

View File

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

View File

@ -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();

View File

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

View File

@ -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%;
}
}

View File

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

View File

@ -0,0 +1,53 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<cdk-virtual-scroll-viewport #viewport class="tb-scroll-grid-viewport" [itemSize]="itemSize" appendOnly>
<ng-container *cdkVirtualFor="let itemsRow of dataSource">
<div *ngIf="itemsRow" class="tb-scroll-grid-items-row" [style]="{gap: gap+'px'}">
<div *ngFor="let item of itemsRow" class="tb-scroll-grid-item-container">
<ng-container *ngIf="item === 'loadingCell'">
<ng-container *ngTemplateOutlet="loadingCell ? loadingCell : defaultLoadingCell"></ng-container>
</ng-container>
<ng-container *ngIf="isObject(item)">
<ng-container *ngTemplateOutlet="itemCard; context:{ item: item }"></ng-container>
</ng-container>
</div>
</div>
</ng-container>
</cdk-virtual-scroll-viewport>
<ng-container *ngIf="dataSource.isEmpty">
<ng-container *ngTemplateOutlet="loadingItems"></ng-container>
</ng-container>
<ng-template #loadingItems>
<ng-container *ngIf="dataSource.initialDataLoading; else emptyData">
<ng-container *ngTemplateOutlet="dataLoading ? dataLoading : defaultDataLoading"></ng-container>
</ng-container>
</ng-template>
<ng-template #emptyData>
<ng-container *ngTemplateOutlet="noData"></ng-container>
</ng-template>
<ng-template #defaultLoadingCell>
<div fxLayout="column" fxLayoutAlign="center center" [style]="{minHeight: itemSize + 'px'}">
<mat-spinner color="accent" strokeWidth="5"></mat-spinner>
</div>
</ng-template>
<ng-template #defaultDataLoading>
<div fxLayout="column"
fxLayoutAlign="center center" class="tb-absolute-fill">
<mat-spinner color="accent" strokeWidth="5"></mat-spinner>
</div>
</ng-template>

View File

@ -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;
}
}

View File

@ -0,0 +1,103 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterViewInit,
Component,
Input,
OnChanges,
OnInit,
Renderer2,
SimpleChanges,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
GridEntitiesFetchFunction,
ScrollGridColumns,
ScrollGridDatasource
} from '@home/models/datasource/scroll-grid-datasource';
import { BreakpointObserver } from '@angular/cdk/layout';
import { isObject } from '@app/core/utils';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({
selector: 'tb-scroll-grid',
templateUrl: './scroll-grid.component.html',
styleUrls: ['./scroll-grid.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ScrollGridComponent<T, F> implements OnInit, AfterViewInit, OnChanges {
@ViewChild('viewport')
viewport: CdkVirtualScrollViewport;
@Input()
columns: ScrollGridColumns = {columns: 1};
@Input()
fetchFunction: GridEntitiesFetchFunction<T, F>;
@Input()
filter: F;
@Input()
itemSize = 200;
@Input()
gap = 12;
@Input()
itemCard: TemplateRef<{item: T}>;
@Input()
loadingCell: TemplateRef<any>;
@Input()
dataLoading: TemplateRef<any>;
@Input()
noData: TemplateRef<any>;
dataSource: ScrollGridDatasource<T, F>;
constructor(private breakpointObserver: BreakpointObserver,
private renderer: Renderer2) {
}
ngOnInit(): void {
this.dataSource = new ScrollGridDatasource<T, F>(this.breakpointObserver, this.columns, this.fetchFunction, this.filter);
}
ngAfterViewInit() {
this.renderer.setStyle(this.viewport._contentWrapper.nativeElement, 'gap', this.gap + 'px');
this.renderer.setStyle(this.viewport._contentWrapper.nativeElement, 'padding', this.gap + 'px');
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue && propName === 'filter') {
this.dataSource.updateFilter(this.filter);
}
}
}
isObject(value: any): boolean {
return isObject(value);
}
}

View File

@ -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,

View File

@ -0,0 +1,250 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { DataSource, ListRange } from '@angular/cdk/collections';
import { CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { BreakpointObserver } from '@angular/cdk/layout';
export type GridEntitiesFetchFunction<T, F> = (pageSize: number, page: number, filter: F) => Observable<PageData<T>>;
export type GridCellType = 'emptyCell' | 'loadingCell';
export interface ScrollGridColumns {
columns: number;
breakpoints?: {[breakpoint: string]: number};
}
export class ScrollGridDatasource<T, F> extends DataSource<(T | GridCellType)[]> {
public initialDataLoading = true;
private _data: T[] = [];
private _rows: (T | GridCellType)[][] = Array.from<T[]>({length: 100000});
private _hasNext = true;
private _columns: number;
private _viewport: CdkVirtualScrollViewport;
private _pendingRange: ListRange = null;
private _fetchingData = false;
private _fetchSubscription: Subscription;
private _totalElements = 0;
private _dataStream: BehaviorSubject<(T | GridCellType)[][]>;
private _subscription: Subscription;
constructor(private breakpointObserver: BreakpointObserver,
private columns: ScrollGridColumns,
private fetchFunction: GridEntitiesFetchFunction<T, F>,
private filter: F) {
super();
}
connect(collectionViewer: CdkVirtualForOf<(T | GridCellType)[]>): Observable<(T | GridCellType)[][]> {
this._viewport = (collectionViewer as any)._viewport;
this._init();
if (this.columns.breakpoints) {
const breakpoints = Object.keys(this.columns.breakpoints);
this._subscription.add(this.breakpointObserver.observe(breakpoints).subscribe(
() => {
this._columnsChanged(this._detectColumns());
}
));
}
this._subscription.add(
collectionViewer.viewChange.subscribe(range => this._fetchDataFromRange(range))
);
return this._dataStream;
}
disconnect(): void {
this._reset();
this._subscription.unsubscribe();
}
get isEmpty(): boolean {
return !this._data.length;
}
get active(): boolean {
return !!this._subscription && !this._subscription.closed;
}
public updateFilter(filter: F) {
this.filter = filter;
if (this.active) {
const prevLength = this._rows.length;
this._reset();
const dataLengthChanged = prevLength !== this._rows.length;
const range = this._viewport.getRenderedRange();
if (dataLengthChanged) {
// Force recalculate new range
if (range.start === 0) {
range.start = 1;
}
this._viewport.appendOnly = false;
}
const scrollOffset = this._viewport.measureScrollOffset();
if (scrollOffset > 0) {
this._viewport.scrollToOffset(0);
}
this._dataUpdated();
this._viewport.appendOnly = true;
if (!dataLengthChanged) {
this._fetchDataFromRange(range);
}
}
}
private _detectColumns(): number {
let columns = this.columns.columns;
if (this.columns.breakpoints) {
for (const breakpont of Object.keys(this.columns.breakpoints)) {
if (this.breakpointObserver.isMatched(breakpont)) {
columns = this.columns.breakpoints[breakpont];
break;
}
}
}
return columns;
}
private _init() {
this._subscription = new Subscription();
this._columns = this._detectColumns();
if (this._dataStream) {
this._dataStream.complete();
}
this._dataStream = new BehaviorSubject(this._rows);
}
private _reset() {
this._data = [];
this._totalElements = 0;
this.initialDataLoading = true;
this._rows = Array.from<T[]>({length: 100000});
this._hasNext = true;
this._pendingRange = null;
this._fetchingData = false;
if (this._fetchSubscription) {
this._fetchSubscription.unsubscribe();
}
}
private _columnsChanged(columns: number) {
if (this._columns !== columns) {
const fetchData = columns > this._columns;
this._columns = columns;
const rowsLength = this._totalElements ? Math.ceil(this._totalElements / this._columns) : 100000;
this._rows = Array.from<T[]>({length: rowsLength});
this._dataUpdated();
if (fetchData && this._hasNext) {
this._fetchDataFromRange(this._viewport.getRenderedRange());
}
}
}
private _fetchDataFromRange(range: ListRange) {
if (this._hasNext) {
if (this._fetchingData) {
this._pendingRange = range;
} else {
const endIndex = (range.end + 1) * this._columns;
if (endIndex > this._data.length) {
const startIndex = this._data.length;
const minPageSize = endIndex - startIndex;
const maxPageSize = minPageSize * 2;
let pageSize = minPageSize;
let page = Math.floor(startIndex / pageSize);
while (startIndex % pageSize !== 0 && pageSize <= maxPageSize) {
if (((page + 1) * pageSize) > endIndex) {
break;
}
pageSize++;
page = Math.floor(startIndex / pageSize);
}
const offset = startIndex % pageSize;
this._fetchData(offset, pageSize, page);
}
}
}
}
private _fetchData(offset: number, pageSize: number, page: number) {
this._fetchingData = true;
this._fetchSubscription = this.fetchFunction(pageSize, page, this.filter).pipe(
catchError(() => of(emptyPageData<T>()))
).subscribe(
(data) => {
this._hasNext = data.hasNext;
if (data.data.length > offset) {
for (let i = offset; i < data.data.length; i++) {
this._data.push(data.data[i]);
}
}
this._totalElements = data.totalElements;
const rowsLength = this._totalElements ? Math.ceil(this._totalElements / this._columns) : 100000;
this._rows = Array.from<T[]>({length: rowsLength});
this._dataUpdated();
this.initialDataLoading = false;
this._fetchingData = false;
if (this._pendingRange) {
const range = this._pendingRange;
this._pendingRange = null;
this._fetchDataFromRange(range);
}
}
);
}
private _dataUpdated() {
for (let index = 0; index < this._data.length; index++) {
const row = Math.floor(index / this._columns);
const col = index % this._columns;
if (!this._rows[row]) {
this._rows[row] = [];
}
this._rows[row][col] = this._data[index];
}
this._fillGridCells();
this._dataStream.next(this._rows);
}
private _fillGridCells() {
if (this._totalElements) {
const startIndex = this._data.length;
const endIndex = this._rows.length * this._columns;
for (let index = startIndex; index < endIndex; index++) {
const row = Math.floor(index / this._columns);
const col = index % this._columns;
const cellType: GridCellType = index < this._totalElements ? 'loadingCell' : 'emptyCell';
if (!this._rows[row]) {
this._rows[row] = [];
}
this._rows[row][col] = cellType;
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View File

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

View File

@ -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",