Browse Source

Merge branch 'resourcesByType' of github.com:dashevchenko/thingsboard into develop/3.5.2

pull/8731/head
Andrii Shvaika 3 years ago
parent
commit
1b15e78985
  1. 7
      application/src/main/data/upgrade/3.5.1/schema_update.sql
  2. 3
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  3. 84
      application/src/main/java/org/thingsboard/server/controller/TbResourceController.java
  4. 14
      application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java
  5. 5
      application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java
  6. 7
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  7. 233
      application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java
  8. 68
      application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java
  9. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java
  10. 15
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java
  11. 2
      common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java
  12. 5
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java
  13. 29
      common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java
  14. 1
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  15. 6
      dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java
  16. 6
      dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java
  17. 11
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java
  18. 5
      dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java
  19. 15
      dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java
  20. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java
  21. 1
      dao/src/main/resources/sql/schema-entities.sql
  22. 6
      dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java
  23. 4
      ui-ngx/package.json
  24. 16
      ui-ngx/src/app/core/http/resource.service.ts
  25. 89
      ui-ngx/src/app/core/services/resources.service.ts
  26. 7
      ui-ngx/src/app/modules/common/modules-map.ts
  27. 32
      ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html
  28. 2
      ui-ngx/src/app/modules/home/pages/admin/admin.module.ts
  29. 15
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts
  30. 6
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html
  31. 45
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts
  32. 30
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html
  33. 42
      ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts
  34. 31
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  35. 27
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss
  36. 1
      ui-ngx/src/app/shared/components/public-api.ts
  37. 46
      ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html
  38. 185
      ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.ts
  39. 19
      ui-ngx/src/app/shared/models/resource.models.ts
  40. 3
      ui-ngx/src/app/shared/shared.module.ts
  41. 9
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  42. 16
      ui-ngx/yarn.lock

7
application/src/main/data/upgrade/3.5.1/schema_update.sql

@ -52,3 +52,10 @@ $$
$$;
-- NOTIFICATION CONFIGS VERSION CONTROL END
ALTER TABLE resource
ADD COLUMN IF NOT EXISTS hash_code varchar;
UPDATE resource
SET hash_code = encode(sha256(decode(resource.data, 'base64')),'hex') WHERE resource.data is not null;

3
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -140,6 +140,9 @@ public class ControllerConstants {
protected static final String RESOURCE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the resource title.";
protected static final String RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, resourceType, tenantId";
protected static final String RESOURCE_TYPE_PROPERTY_ALLOWABLE_VALUES = "LWM2M_MODEL, JKS, PKCS_12, JS_MODULE";
protected static final String RESOURCE_TYPE = "A string value representing the resource type.";
protected static final String LWM2M_OBJECT_DESCRIPTION = "LwM2M Object is a object that includes information about the LwM2M model which can be used in transport configuration for the LwM2M device profile. ";
protected static final String LWM2M_OBJECT_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name";

84
application/src/main/java/org/thingsboard/server/controller/TbResourceController.java

@ -15,24 +15,32 @@
*/
package org.thingsboard.server.controller;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
@ -41,6 +49,7 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
@ -57,6 +66,8 @@ import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_ID_
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.RESOURCE_TYPE_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
@ -94,6 +105,42 @@ public class TbResourceController extends BaseController {
.body(resource);
}
@ApiOperation(value = "Download LWM2M Resource (downloadLwm2mResourceIfChanged)", notes = "Download Resource based on the provided Resource Id or return 304 status code if resource was not changed." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/lwm2m/{resourceId}/download", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadLwm2mResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.LWM2M_MODEL, strResourceId, etag);
}
@ApiOperation(value = "Download PKCS_12 Resource (downloadPkcs12ResourceIfChanged)", notes = "Download Resource based on the provided Resource Id or return 304 status code if resource was not changed." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/pkcs12/{resourceId}/download", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadPkcs12ResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.PKCS_12, strResourceId, etag);
}
@ApiOperation(value = "Download JKS Resource (downloadJksResourceIfChanged)", notes = "Download Resource based on the provided Resource Id or return 304 status code if resource was not changed." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/resource/jks/{resourceId}/download", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadJksResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JKS, strResourceId, etag);
}
@ApiOperation(value = "Download JS Resource (downloadJsResourceIfChanged)", notes = "Download Resource based on the provided Resource Id or return 304 status code if resource was not changed." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/resource/js/{resourceId}/download", method = RequestMethod.GET)
@ResponseBody
public ResponseEntity<org.springframework.core.io.Resource> downloadJsResourceIfChanged(@ApiParam(value = RESOURCE_ID_PARAM_DESCRIPTION)
@PathVariable(RESOURCE_ID) String strResourceId, @RequestHeader(name = HttpHeaders.IF_NONE_MATCH, required = false) String etag) throws ThingsboardException {
return downloadResourceIfChanged(ResourceType.JS_MODULE, strResourceId, etag);
}
@ApiOperation(value = "Get Resource Info (getResourceInfoById)",
notes = "Fetch the Resource Info object based on the provided Resource Id. " +
RESOURCE_INFO_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH,
@ -153,6 +200,8 @@ public class TbResourceController extends BaseController {
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = RESOURCE_TYPE, allowableValues = RESOURCE_TYPE_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String resourceType,
@ApiParam(value = RESOURCE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = RESOURCE_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -160,10 +209,15 @@ public class TbResourceController extends BaseController {
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
TbResourceInfoFilter.TbResourceInfoFilterBuilder filter = TbResourceInfoFilter.builder();
filter.tenantId(getTenantId());
if (StringUtils.isNotEmpty(resourceType)){
filter.resourceType(ResourceType.valueOf(resourceType));
}
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(resourceService.findTenantResourcesByTenantId(getTenantId(), pageLink));
return checkNotNull(resourceService.findTenantResourcesByTenantId(filter.build(), pageLink));
} else {
return checkNotNull(resourceService.findAllTenantResourcesByTenantId(getTenantId(), pageLink));
return checkNotNull(resourceService.findAllTenantResourcesByTenantId(filter.build(), pageLink));
}
}
@ -216,4 +270,30 @@ public class TbResourceController extends BaseController {
TbResource tbResource = checkResourceId(resourceId, Operation.DELETE);
tbResourceService.delete(tbResource, getCurrentUser());
}
private ResponseEntity<org.springframework.core.io.Resource> downloadResourceIfChanged(ResourceType type, String strResourceId, String etag) throws ThingsboardException {
checkParameter(RESOURCE_ID, strResourceId);
TbResourceId resourceId = new TbResourceId(toUUID(strResourceId));
if (etag != null) {
TbResourceInfo tbResourceInfo = checkResourceInfoId(resourceId, Operation.READ);
if (etag.equals(tbResourceInfo.getHashCode())) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED)
.eTag(tbResourceInfo.getHashCode())
.build();
}
}
TbResource tbResource = checkResourceId(resourceId, Operation.READ);
ByteArrayResource resource = new ByteArrayResource(Base64.getDecoder().decode(tbResource.getData().getBytes()));
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + tbResource.getFileName())
.header("x-filename", tbResource.getFileName())
.contentLength(resource.contentLength())
.header("Content-Type", type.getMediaType())
.cacheControl(CacheControl.noCache())
.eTag(tbResource.getHashCode())
.body(resource);
}
}

14
application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.service.resource;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
@ -22,6 +24,7 @@ import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
@ -35,6 +38,7 @@ import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import java.util.Base64;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@ -72,13 +76,13 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
}
@Override
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) {
return resourceService.findAllTenantResourcesByTenantId(tenantId, pageLink);
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
return resourceService.findAllTenantResourcesByTenantId(filter, pageLink);
}
@Override
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) {
return resourceService.findTenantResourcesByTenantId(tenantId, pageLink);
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
return resourceService.findTenantResourcesByTenantId(filter, pageLink);
}
@Override
@ -165,6 +169,8 @@ public class DefaultTbResourceService extends AbstractTbEntityService implements
} else {
resource.setResourceKey(resource.getFileName());
}
HashCode hashCode = Hashing.sha256().hashBytes(Base64.getDecoder().decode(resource.getData().getBytes()));
resource.setHashCode(hashCode.toString());
return resourceService.saveResource(resource);
}
}

5
application/src/main/java/org/thingsboard/server/service/resource/TbResourceService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.resource;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.lwm2m.LwM2mObject;
@ -35,9 +36,9 @@ public interface TbResourceService extends SimpleTbEntityService<TbResource> {
TbResourceInfo findResourceInfoById(TenantId tenantId, TbResourceId resourceId);
PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
PageData<TbResourceInfo> findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
List<LwM2mObject> findLwM2mObject(TenantId tenantId,
String sortOrder,

7
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -601,6 +601,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return mockMvc.perform(getRequest);
}
protected ResultActions doGet(String urlTemplate, HttpHeaders httpHeaders, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
getRequest.headers(httpHeaders);
setJwtToken(getRequest);
return mockMvc.perform(getRequest);
}
protected <T> T doGet(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception {
return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
}

233
application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java

File diff suppressed because one or more lines are too long

68
application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java

File diff suppressed because one or more lines are too long

5
common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TbResourceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@ -39,9 +40,9 @@ public interface ResourceService extends EntityDaoService {
ListenableFuture<TbResourceInfo> findResourceInfoByIdAsync(TenantId tenantId, TbResourceId resourceId);
PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
PageData<TbResourceInfo> findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
List<TbResource> findTenantResourcesByResourceTypeAndObjectIds(TenantId tenantId, ResourceType lwm2mModel, String[] objectIds);

15
common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java

@ -16,5 +16,18 @@
package org.thingsboard.server.common.data;
public enum ResourceType {
LWM2M_MODEL, JKS, PKCS_12
LWM2M_MODEL("application/xml"),
JKS("application/x-java-keystore"),
PKCS_12("application/x-pkcs12"),
JS_MODULE("application/javascript");
private final String mediaType;
ResourceType(String mediaType) {
this.mediaType = mediaType;
}
public String getMediaType() {
return mediaType;
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java

@ -74,6 +74,8 @@ public class TbResource extends TbResourceInfo {
builder.append(fileName);
builder.append(", data=");
builder.append(data);
builder.append(", hashCode=");
builder.append(getHashCode());
builder.append("]");
return builder.toString();
}

5
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java

@ -48,6 +48,8 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
private String resourceKey;
@ApiModelProperty(position = 7, value = "Resource search text.", example = "19_1.0:binaryappdatacontainer", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String searchText;
@ApiModelProperty(position = 8, value = "Resource hash code.", example = "33a64df551425fcc55e4d42a148795d9f25f89d4", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String hashCode;
public TbResourceInfo() {
super();
@ -64,6 +66,7 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
this.resourceType = resourceInfo.getResourceType();
this.resourceKey = resourceInfo.getResourceKey();
this.searchText = resourceInfo.getSearchText();
this.hashCode = resourceInfo.getHashCode();
}
@ApiModelProperty(position = 1, value = "JSON object with the Resource Id. " +
@ -107,6 +110,8 @@ public class TbResourceInfo extends BaseData<TbResourceId> implements HasName, H
builder.append(resourceType);
builder.append(", resourceKey=");
builder.append(resourceKey);
builder.append(", hashCode=");
builder.append(hashCode);
builder.append("]");
return builder.toString();
}

29
common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfoFilter.java

@ -0,0 +1,29 @@
/**
* 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;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@Builder
public class TbResourceInfoFilter {
private TenantId tenantId;
private ResourceType resourceType;
}

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

@ -480,6 +480,7 @@ public class ModelConstants {
public static final String RESOURCE_TITLE_COLUMN = TITLE_PROPERTY;
public static final String RESOURCE_FILE_NAME_COLUMN = "file_name";
public static final String RESOURCE_DATA_COLUMN = "data";
public static final String RESOURCE_HASH_CODE_COLUMN = "hash_code";
/**
* Ota Package constants.

6
dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceEntity.java

@ -31,6 +31,7 @@ import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_DATA_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_FILE_NAME_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_HASH_CODE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
@ -65,6 +66,9 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseE
@Column(name = RESOURCE_DATA_COLUMN)
private String data;
@Column(name = RESOURCE_HASH_CODE_COLUMN)
private String hashCode;
public TbResourceEntity() {
}
@ -82,6 +86,7 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseE
this.searchText = resource.getSearchText();
this.fileName = resource.getFileName();
this.data = resource.getData();
this.hashCode = resource.getHashCode();
}
@Override
@ -95,6 +100,7 @@ public class TbResourceEntity extends BaseSqlEntity<TbResource> implements BaseE
resource.setSearchText(searchText);
resource.setFileName(fileName);
resource.setData(data);
resource.setHashCode(hashCode);
return resource;
}

6
dao/src/main/java/org/thingsboard/server/dao/model/sql/TbResourceInfoEntity.java

@ -29,6 +29,7 @@ import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.UUID;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_HASH_CODE_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_KEY_COLUMN;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.RESOURCE_TENANT_ID_COLUMN;
@ -57,6 +58,9 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
@Column(name = SEARCH_TEXT_PROPERTY)
private String searchText;
@Column(name = RESOURCE_HASH_CODE_COLUMN)
private String hashCode;
public TbResourceInfoEntity() {
}
@ -70,6 +74,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
this.resourceType = resource.getResourceType().name();
this.resourceKey = resource.getResourceKey();
this.searchText = resource.getSearchText();
this.hashCode = resource.getHashCode();
}
@Override
@ -81,6 +86,7 @@ public class TbResourceInfoEntity extends BaseSqlEntity<TbResourceInfo> implemen
resource.setResourceType(ResourceType.valueOf(resourceType));
resource.setResourceKey(resourceKey);
resource.setSearchText(searchText);
resource.setHashCode(hashCode);
return resource;
}
}

11
dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TbResourceId;
@ -103,17 +104,19 @@ public class BaseResourceService implements ResourceService {
}
@Override
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) {
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
TenantId tenantId = filter.getTenantId();
log.trace("Executing findAllTenantResourcesByTenantId [{}]", tenantId);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
return resourceInfoDao.findAllTenantResourcesByTenantId(tenantId.getId(), pageLink);
return resourceInfoDao.findAllTenantResourcesByTenantId(filter, pageLink);
}
@Override
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TenantId tenantId, PageLink pageLink) {
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
TenantId tenantId = filter.getTenantId();
log.trace("Executing findTenantResourcesByTenantId [{}]", tenantId);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
return resourceInfoDao.findTenantResourcesByTenantId(tenantId.getId(), pageLink);
return resourceInfoDao.findTenantResourcesByTenantId(filter, pageLink);
}
@Override

5
dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.resource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
@ -24,8 +25,8 @@ import java.util.UUID;
public interface TbResourceInfoDao extends Dao<TbResourceInfo> {
PageData<TbResourceInfo> findAllTenantResourcesByTenantId(UUID tenantId, PageLink pageLink);
PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
PageData<TbResourceInfo> findTenantResourcesByTenantId(UUID tenantId, PageLink pageLink);
PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink);
}

15
dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceInfoDao.java

@ -19,7 +19,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -51,21 +53,26 @@ public class JpaTbResourceInfoDao extends JpaAbstractDao<TbResourceInfoEntity, T
}
@Override
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(UUID tenantId, PageLink pageLink) {
public PageData<TbResourceInfo> findAllTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
ResourceType resourceType = filter.getResourceType();
return DaoUtil.toPageData(resourceInfoRepository
.findAllTenantResourcesByTenantId(
tenantId,
filter.getTenantId().getId(),
TenantId.NULL_UUID,
resourceType == null ? null : resourceType.name(),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<TbResourceInfo> findTenantResourcesByTenantId(UUID tenantId, PageLink pageLink) {
public PageData<TbResourceInfo> findTenantResourcesByTenantId(TbResourceInfoFilter filter, PageLink pageLink) {
ResourceType resourceType = filter.getResourceType();
return DaoUtil.toPageData(resourceInfoRepository
.findTenantResourcesByTenantId(
tenantId,
filter.getTenantId().getId(),
resourceType == null ? null : resourceType.name(),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
}

7
dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java

@ -34,16 +34,21 @@ public interface TbResourceInfoRepository extends JpaRepository<TbResourceInfoEn
"(SELECT sr FROM TbResourceEntity sr " +
"WHERE sr.tenantId = :tenantId " +
"AND tr.resourceType = sr.resourceType " +
"AND tr.resourceKey = sr.resourceKey)))")
"AND tr.resourceKey = sr.resourceKey)))" +
"AND (:resourceType IS NULL OR tr.resourceType = :resourceType)")
Page<TbResourceInfoEntity> findAllTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("systemAdminId") UUID sysadminId,
@Param("resourceType") String resourceType,
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT ri FROM TbResourceInfoEntity ri WHERE " +
"ri.tenantId = :tenantId " +
"AND (:resourceType IS NULL OR ri.resourceType = :resourceType)" +
"AND LOWER(ri.title) LIKE LOWER(CONCAT('%', :searchText, '%'))")
Page<TbResourceInfoEntity> findTenantResourcesByTenantId(@Param("tenantId") UUID tenantId,
@Param("resourceType") String resourceType,
@Param("searchText") String searchText,
Pageable pageable);
}

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

@ -697,6 +697,7 @@ CREATE TABLE IF NOT EXISTS resource (
search_text varchar(255),
file_name varchar(255) NOT NULL,
data varchar,
hash_code varchar,
CONSTRAINT resource_unq_key UNIQUE (tenant_id, resource_type, resource_key)
);

6
dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.ResourceType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.TbResourceInfoFilter;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.TenantProfile;
@ -505,8 +506,11 @@ public class TenantServiceTest extends AbstractServiceTest {
assertThat(resourceService.findResourceById(tenant.getId(), resource.getId()))
.as("resource").isNull();
PageLink pageLinkResources = new PageLink(1);
TbResourceInfoFilter filter = TbResourceInfoFilter.builder()
.tenantId(tenantId)
.build();
PageData<TbResourceInfo> tenantResources =
resourceService.findAllTenantResourcesByTenantId(tenant.getId(), pageLinkResources);
resourceService.findAllTenantResourcesByTenantId(filter, pageLinkResources);
Assert.assertEquals(0, tenantResources.getTotalElements());
}

4
ui-ngx/package.json

@ -96,7 +96,7 @@
"schema-inspector": "^2.0.2",
"screenfull": "^6.0.2",
"split.js": "^1.6.5",
"systemjs": "6.11.0",
"systemjs": "6.14.1",
"tinycolor2": "^1.6.0",
"tinymce": "~5.10.7",
"tooltipster": "^4.2.8",
@ -137,7 +137,7 @@
"@types/raphael": "^2.3.2",
"@types/react": "17.0.37",
"@types/react-dom": "17.0.11",
"@types/systemjs": "6.1.1",
"@types/systemjs": "6.13.1",
"@types/tinycolor2": "^1.4.3",
"@types/tooltipster": "^0.0.31",
"@typescript-eslint/eslint-plugin": "5.57.0",

16
ui-ngx/src/app/core/http/resource.service.ts

@ -20,8 +20,9 @@ import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { forkJoin, Observable, of } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { Resource, ResourceInfo } from '@shared/models/resource.models';
import { Resource, ResourceInfo, ResourceType } from '@shared/models/resource.models';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { isNotEmptyStr } from '@core/utils';
@Injectable({
providedIn: 'root'
@ -33,15 +34,22 @@ export class ResourceService {
}
public getResources(pageLink: PageLink, config?: RequestConfig): Observable<PageData<ResourceInfo>> {
return this.http.get<PageData<ResourceInfo>>(`/api/resource${pageLink.toQuery()}`,
defaultHttpOptionsFromConfig(config));
public getResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable<PageData<ResourceInfo>> {
let url = `/api/resource${pageLink.toQuery()}`;
if (isNotEmptyStr(resourceType)) {
url += `&resourceType=${resourceType}`;
}
return this.http.get<PageData<ResourceInfo>>(url, defaultHttpOptionsFromConfig(config));
}
public getResource(resourceId: string, config?: RequestConfig): Observable<Resource> {
return this.http.get<Resource>(`/api/resource/${resourceId}`, defaultHttpOptionsFromConfig(config));
}
public getResourceInfo(resourceId: string, config?: RequestConfig): Observable<ResourceInfo> {
return this.http.get<Resource>(`/api/resource/info/${resourceId}`, defaultHttpOptionsFromConfig(config));
}
public downloadResource(resourceId: string): Observable<any> {
return this.http.get(`/api/resource/${resourceId}/download`, {
responseType: 'arraybuffer',

89
ui-ngx/src/app/core/services/resources.service.ts

@ -27,6 +27,13 @@ import { DOCUMENT } from '@angular/common';
import { forkJoin, Observable, ReplaySubject, throwError } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { IModulesMap } from '@modules/common/modules-map.models';
import { TbResourceId } from '@shared/models/id/tb-resource-id';
import { isObject } from '@core/utils';
import { AuthService } from '@core/auth/auth.service';
import { select, Store } from '@ngrx/store';
import { selectIsAuthenticated } from '@core/auth/auth.selectors';
import { AppState } from '@core/core.state';
import { tap } from 'rxjs/operators';
declare const System;
@ -47,9 +54,12 @@ export class ResourcesService {
private anchor = this.document.getElementsByTagName('head')[0] || this.document.getElementsByTagName('body')[0];
constructor(@Inject(DOCUMENT) private readonly document: any,
protected store: Store<AppState>,
private compiler: Compiler,
private http: HttpClient,
private injector: Injector) {}
private injector: Injector) {
this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache());
}
public loadResource(url: string): Observable<any> {
if (this.loadedResources[url]) {
@ -62,23 +72,25 @@ export class ResourcesService {
fileType = match[1];
}
if (!fileType) {
return throwError(new Error(`Unable to detect file type from url: ${url}`));
return throwError(() => new Error(`Unable to detect file type from url: ${url}`));
} else if (fileType !== 'css' && fileType !== 'js') {
return throwError(new Error(`Unsupported file type: ${fileType}`));
return throwError(() => new Error(`Unsupported file type: ${fileType}`));
}
return this.loadResourceByType(fileType, url);
}
public loadFactories(url: string, modulesMap: IModulesMap): Observable<ModulesWithFactories> {
public loadFactories(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<ModulesWithFactories> {
const url = this.getDownloadUrl(resourceId);
if (this.loadedModulesAndFactories[url]) {
return this.loadedModulesAndFactories[url].asObservable();
}
modulesMap.init();
const meta = this.getMetaInfo(resourceId);
const subject = new ReplaySubject<ModulesWithFactories>();
this.loadedModulesAndFactories[url] = subject;
import('@angular/compiler').then(
() => {
System.import(url).then(
System.import(url, undefined, meta).then(
(module) => {
const modules = this.extractNgModules(module);
if (modules.length) {
@ -86,7 +98,8 @@ export class ResourcesService {
for (const m of modules) {
tasks.push(this.compiler.compileModuleAndAllComponentsAsync(m));
}
forkJoin(tasks).subscribe((compiled) => {
forkJoin(tasks).subscribe({
next: (compiled) => {
try {
const componentFactories: ComponentFactory<any>[] = [];
for (const c of compiled) {
@ -101,38 +114,46 @@ export class ResourcesService {
this.loadedModulesAndFactories[url].complete();
} catch (e) {
this.loadedModulesAndFactories[url].error(new Error(`Unable to init module from url: ${url}`));
delete this.loadedModulesAndFactories[url];
}
},
(e) => {
error: (e) => {
this.loadedModulesAndFactories[url].error(new Error(`Unable to compile module from url: ${url}`));
delete this.loadedModulesAndFactories[url];
});
}
});
} else {
this.loadedModulesAndFactories[url].error(new Error(`Module '${url}' doesn't have default export!`));
delete this.loadedModulesAndFactories[url];
}
},
(e) => {
this.loadedModulesAndFactories[url].error(new Error(`Unable to load module from url: ${url}`));
delete this.loadedModulesAndFactories[url];
}
);
}
);
return subject.asObservable();
return subject.asObservable().pipe(
tap({
next: () => System.delete(url),
error: () => {
delete this.loadedModulesAndFactories[url];
System.delete(url);
},
complete: () => System.delete(url)
})
);
}
public loadModules(url: string, modulesMap: IModulesMap): Observable<Type<any>[]> {
public loadModules(resourceId: string | TbResourceId, modulesMap: IModulesMap): Observable<Type<any>[]> {
const url = this.getDownloadUrl(resourceId);
if (this.loadedModules[url]) {
return this.loadedModules[url].asObservable();
}
modulesMap.init();
const meta = this.getMetaInfo(resourceId);
const subject = new ReplaySubject<Type<any>[]>();
this.loadedModules[url] = subject;
import('@angular/compiler').then(
() => {
System.import(url).then(
System.import(url, undefined, meta).then(
(module) => {
try {
let modules;
@ -155,31 +176,35 @@ export class ResourcesService {
this.loadedModules[url].complete();
} catch (e) {
this.loadedModules[url].error(new Error(`Unable to init module from url: ${url}`));
delete this.loadedModules[url];
}
},
(e) => {
this.loadedModules[url].error(new Error(`Unable to compile module from url: ${url}`));
delete this.loadedModules[url];
});
} else {
this.loadedModules[url].error(new Error(`Module '${url}' doesn't have default export or not NgModule!`));
delete this.loadedModules[url];
}
} catch (e) {
this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`));
delete this.loadedModules[url];
}
},
(e) => {
this.loadedModules[url].error(new Error(`Unable to load module from url: ${url}`));
delete this.loadedModules[url];
console.error(`Unable to load module from url: ${url}`, e);
}
);
}
);
return subject.asObservable();
return subject.asObservable().pipe(
tap({
next: () => System.delete(url),
error: () => {
delete this.loadedModulesAndFactories[url];
System.delete(url);
},
complete: () => System.delete(url)
})
);
}
private extractNgModules(module: any, modules: Type<any>[] = []): Type<any>[] {
@ -246,4 +271,26 @@ export class ResourcesService {
this.anchor.appendChild(el);
return subject.asObservable();
}
private getDownloadUrl(resourceId: string | TbResourceId): string {
if (isObject(resourceId)) {
return `/api/resource/js/${(resourceId as TbResourceId).id}/download`;
}
return resourceId as string;
}
private getMetaInfo(resourceId: string | TbResourceId): object {
if (isObject(resourceId)) {
return {
additionalHeaders: {
'X-Authorization': `Bearer ${AuthService.getJwtToken()}`
}
};
}
}
private clearModulesCache() {
this.loadedModules = {};
this.loadedModulesAndFactories = {};
}
}

7
ui-ngx/src/app/modules/common/modules-map.ts

@ -616,6 +616,13 @@ class ModulesMap implements IModulesMap {
for (const moduleId of Object.keys(this.modulesMap)) {
System.set('app:' + moduleId, this.modulesMap[moduleId]);
}
System.constructor.prototype.shouldFetch = (url: string) => url.endsWith('/download');
System.constructor.prototype.fetch = (url, options: RequestInit & {meta?: any}) => {
if (options?.meta?.additionalHeaders) {
options.headers = { ...options.headers, ...options.meta.additionalHeaders };
}
return fetch(url, options);
};
this.initialized = true;
}
}

32
ui-ngx/src/app/modules/home/components/widget/action/custom-action-pretty-resources-tabs.component.html

@ -22,11 +22,14 @@
<div fxFlex fxLayout="row"
fxLayoutAlign="start center"
*ngFor="let resource of action.customResources; let i = index" style="padding-top: 10px;">
<mat-form-field fxFlex class="mat-block" subscriptSizing="dynamic">
<input required matInput [(ngModel)]="resource.url"
(ngModelChange)="notifyActionUpdated()"
placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field>
<tb-resource-autocomplete fxFlex class="mat-block"
subscriptSizing="dynamic"
hideRequiredMarker required
[allowAutocomplete]="resource.isModule"
[(ngModel)]="resource.url"
(ngModelChange)="notifyActionUpdated()"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="notifyActionUpdated()">
{{ 'widget.resource-is-module' | translate }}
@ -40,16 +43,15 @@
<mat-icon>close</mat-icon>
</button>
</div>
<div style="margin-top: 18px;">
<button mat-button mat-raised-button color="primary"
[disabled]="isLoading$ | async"
type="button"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
<button mat-button mat-raised-button color="primary"
style="margin-top: 18px;"
[disabled]="isLoading$ | async"
type="button"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</div>
</mat-tab>

2
ui-ngx/src/app/modules/home/pages/admin/admin.module.ts

@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component';
import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component';
import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component';
import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component';
import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
import { QueueComponent } from '@home/pages/admin/queue/queue.component';
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
@ -44,6 +45,7 @@ import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-aut
OAuth2SettingsComponent,
HomeSettingsComponent,
ResourcesLibraryComponent,
ResourcesTableHeaderComponent,
QueueComponent,
RepositoryAdminSettingsComponent,
AutoCommitAdminSettingsComponent,

15
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts

@ -36,6 +36,7 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-
import { PageLink } from '@shared/models/page/page-link';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { map } from 'rxjs/operators';
import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component';
@Injectable()
export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableConfig<Resource, PageLink, ResourceInfo>> {
@ -53,6 +54,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.entityComponent = ResourcesLibraryComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.TB_RESOURCE);
this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE);
this.config.headerComponent = ResourcesTableHeaderComponent;
this.config.entityTitle = (resource) => resource ?
resource.title : '';
@ -61,11 +63,9 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
new DateEntityTableColumn<ResourceInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<ResourceInfo>('title', 'resource.title', '60%'),
new EntityTableColumn<ResourceInfo>('resourceType', 'resource.resource-type', '40%',
entity => this.resourceTypesTranslationMap.get(entity.resourceType)),
entity => this.translate.instant(this.resourceTypesTranslationMap.get(entity.resourceType))),
new EntityTableColumn<ResourceInfo>('tenantId', 'resource.system', '60px',
entity => {
return checkBoxCell(entity.tenantId.id === NULL_UUID);
}),
entity => checkBoxCell(entity.tenantId.id === NULL_UUID)),
);
this.config.cellActionDescriptors.push(
@ -83,7 +83,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text');
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink);
this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType);
this.config.loadEntity = id => this.resourceService.getResource(id.id);
this.config.saveEntity = resource => this.saveResource(resource);
this.config.deleteEntity = id => this.resourceService.deleteResource(id.id);
@ -112,6 +112,9 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
resolve(): EntityTableConfig<Resource, PageLink, ResourceInfo> {
this.config.tableTitle = this.translate.instant('resource.resources-library');
this.config.componentsData = {
resourceType: ''
};
const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
this.config.entitySelectionEnabled = (resource) => this.isResourceEditable(resource, authUser.authority);
@ -124,7 +127,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC
$event.stopPropagation();
}
const url = this.router.createUrlTree(['resources', 'resources-library', resourceInfo.id.id]);
this.router.navigateByUrl(url);
this.router.navigateByUrl(url).then(() => {});
}
downloadResource($event: Event, resource: ResourceInfo) {

6
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html

@ -52,7 +52,7 @@
<mat-label translate>resource.resource-type</mat-label>
<mat-select formControlName="resourceType" required>
<mat-option *ngFor="let resourceType of resourceTypes" [value]="resourceType">
{{ resourceTypesTranslationMap.get(resourceType) }}
{{ resourceTypesTranslationMap.get(resourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@ -66,7 +66,7 @@
{{ 'resource.title-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-file-input *ngIf="isAdd"
<tb-file-input *ngIf="isAdd || (isEdit && entityForm.get('resourceType').value === resourceType.JS_MODULE)"
formControlName="data"
required
[readAsBinary]="true"
@ -78,7 +78,7 @@
[existingFileName]="entityForm.get('fileName')?.value"
(fileNameChanged)="entityForm?.get('fileName').patchValue($event)">
</tb-file-input>
<div *ngIf="!isAdd" fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
<div *ngIf="!isAdd && !(isEdit && entityForm.get('resourceType').value === resourceType.JS_MODULE)" fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column">
<mat-form-field fxFlex>
<mat-label translate>resource.file-name</mat-label>
<input matInput formControlName="fileName" type="text">

45
ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.ts

@ -20,7 +20,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { EntityComponent } from '@home/components/entity/entity.component';
import {
Resource,
@ -29,8 +29,9 @@ import {
ResourceTypeMIMETypes,
ResourceTypeTranslationMap
} from '@shared/models/resource.models';
import {filter, pairwise, startWith, takeUntil} from 'rxjs/operators';
import { filter, startWith, takeUntil } from 'rxjs/operators';
import { ActionNotificationShow } from '@core/notification/notification.actions';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-resources-library',
@ -39,7 +40,7 @@ import { ActionNotificationShow } from '@core/notification/notification.actions'
export class ResourcesLibraryComponent extends EntityComponent<Resource> implements OnInit, OnDestroy {
readonly resourceType = ResourceType;
readonly resourceTypes = Object.values(this.resourceType);
readonly resourceTypes: ResourceType[] = Object.values(this.resourceType);
readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
private destroy$ = new Subject<void>();
@ -48,7 +49,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
protected translate: TranslateService,
@Inject('entity') protected entityValue: Resource,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>,
public fb: UntypedFormBuilder,
public fb: FormBuilder,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
@ -56,7 +57,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
ngOnInit() {
super.ngOnInit();
this.entityForm.get('resourceType').valueChanges.pipe(
startWith(ResourceType.LWM2M_MODEL),
startWith(ResourceType.JS_MODULE),
filter(() => this.isAdd),
takeUntil(this.destroy$)
).subscribe((type) => {
@ -87,33 +88,39 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme
}
}
buildForm(entity: Resource): UntypedFormGroup {
const form = this.fb.group(
{
title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, [Validators.required]],
fileName: [entity ? entity.fileName : null, [Validators.required]],
}
);
if (this.isAdd) {
form.addControl('data', this.fb.control(null, Validators.required));
}
return form;
buildForm(entity: Resource): FormGroup {
return this.fb.group({
title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]],
resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.JS_MODULE, Validators.required],
fileName: [entity ? entity.fileName : null, Validators.required],
data: [entity ? entity.data : null, Validators.required]
});
}
updateForm(entity: Resource) {
this.entity.name = entity.title;
if (this.isEdit) {
this.entityForm.get('resourceType').disable({emitEvent: false});
this.entityForm.get('fileName').disable({emitEvent: false});
if (entity.resourceType !== ResourceType.JS_MODULE) {
this.entityForm.get('fileName').disable({emitEvent: false});
this.entityForm.get('data').disable({emitEvent: false});
}
}
this.entityForm.patchValue({
resourceType: entity.resourceType,
fileName: entity.fileName,
title: entity.title
title: entity.title,
data: entity.data
});
}
prepareFormValue(formValue: Resource): Resource {
if (this.isEdit && !isDefinedAndNotNull(formValue.data)) {
delete formValue.data;
}
return super.prepareFormValue(formValue);
}
getAllowedExtensions() {
try {
return ResourceTypeExtension.get(this.entityForm.get('resourceType').value);

30
ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.html

@ -0,0 +1,30 @@
<!--
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.
-->
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>resource.resource-type</mat-label>
<mat-select [ngModel]="entitiesTableConfig.componentsData.resourceType"
(ngModelChange)="resourceTypeChanged($event)"
placeholder="{{ 'resource.resource-type' | translate }}">
<mat-option value="">
{{ "resource.all-types" | translate }}
</mat-option>
<mat-option *ngFor="let resourceType of resourceTypes" [value]="resourceType">
{{ resourceTypesTranslationMap.get(resourceType) | translate }}
</mat-option>
</mat-select>
</mat-form-field>

42
ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts

@ -0,0 +1,42 @@
///
/// 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 { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component';
import { Resource, ResourceInfo, ResourceType, ResourceTypeTranslationMap } from '@shared/models/resource.models';
import { PageLink } from '@shared/models/page/page-link';
@Component({
selector: 'tb-resources-table-header',
templateUrl: './resources-table-header.component.html',
styleUrls: []
})
export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent<Resource, PageLink, ResourceInfo> {
readonly resourceTypes: ResourceType[] = Object.values(ResourceType);
readonly resourceTypesTranslationMap = ResourceTypeTranslationMap;
constructor(protected store: Store<AppState>) {
super(store);
}
resourceTypeChanged(resourceType: ResourceType) {
this.entitiesTableConfig.componentsData.resourceType = resourceType;
this.entitiesTableConfig.getTable().resetSortAndFilter(true);
}
}

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

@ -122,12 +122,14 @@
<div fxFlex fxLayout="row" style="max-height: 40px;"
fxLayoutAlign="start center"
*ngFor="let resource of widget.resources; let i = index" >
<mat-form-field fxFlex class="mat-block resource-field" hideRequiredMarker="false"
style="margin: 10px 0 0 0; max-height: 40px;">
<input required matInput [(ngModel)]="resource.url"
(ngModelChange)="isDirty = true"
placeholder="{{ 'widget.resource-url' | translate }}"/>
</mat-form-field>
<tb-resource-autocomplete fxFlex class="mat-block resource-field"
hideRequiredMarker required
subscriptSizing="dynamic"
[allowAutocomplete]="resource.isModule"
[(ngModel)]="resource.url"
(ngModelChange)="isDirty = true"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
<mat-checkbox [(ngModel)]="resource.isModule"
(ngModelChange)="isDirty = true">
{{ 'widget.resource-is-module' | translate }}
@ -140,15 +142,14 @@
<mat-icon>close</mat-icon>
</button>
</div>
<div style="margin-top: 6px;">
<button mat-raised-button color="primary"
[disabled]="isLoading$ | async"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
<button mat-raised-button color="primary"
style="margin-top: 8px;"
[disabled]="isLoading$ | async"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</div>
</mat-tab>

27
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.scss

@ -31,18 +31,21 @@ tb-widget-editor {
overflow-y: auto;
}
mat-form-field.resource-field {
max-height: 40px;
margin: 10px 0 0;
.mat-mdc-text-field-wrapper {
padding-bottom: 0;
.mat-mdc-form-field-flex {
max-height: 40px;
.mat-mdc-form-field-infix {
border: 0;
padding-top: 7px;
padding-bottom: 7px;
min-height: 32px;
.resource-field {
mat-form-field {
.mat-mdc-text-field-wrapper {
padding-bottom: 0;
height: 40px;
.mat-mdc-form-field-flex {
max-height: 40px;
.mat-mdc-form-field-infix {
border: 0;
padding-top: 7px;
padding-bottom: 7px;
min-height: 32px;
}
}
}
}

1
ui-ngx/src/app/shared/components/public-api.ts

@ -22,4 +22,5 @@ export * from './js-func.component';
export * from './script-lang.component';
export * from './slack-conversation-autocomplete.component';
export * from './notification/template-autocomplete.component';
export * from './resource/resource-autocomplete.component';
export * from './toggle-header.component';

46
ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.html

@ -0,0 +1,46 @@
<!--
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.
-->
<mat-form-field [formGroup]="resourceFormGroup" class="mat-block"
[appearance]="appearance"
[hideRequiredMarker]="hideRequiredMarker"
[subscriptSizing]="subscriptSizing">
<input matInput type="text"
#resourceInput
formControlName="resource"
(focusin)="onFocus()"
[placeholder]="placeholder"
[required]="required"
[matAutocomplete]="entityAutocomplete"
[matAutocompleteDisabled]="!allowAutocomplete">
<button *ngIf="resourceFormGroup.get('resource').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete class="tb-autocomplete"
#entityAutocomplete="matAutocomplete"
[displayWith]="displayResourceFn">
<mat-option *ngFor="let resource of filteredResources$ | async" [value]="resource">
<span [innerHTML]="resource.title | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredResources$ | async)?.length" [value]="searchText">
{{ searchText }}
</mat-option>
</mat-autocomplete>
</mat-form-field>

185
ui-ngx/src/app/shared/components/resource/resource-autocomplete.component.ts

@ -0,0 +1,185 @@
///
/// 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 { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Observable, of } from 'rxjs';
import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
import { isDefinedAndNotNull, isEmptyStr, isEqual, isObject } from '@core/utils';
import { ResourceInfo, ResourceType } from '@shared/models/resource.models';
import { TbResourceId } from '@shared/models/id/tb-resource-id';
import { ResourceService } from '@core/http/resource.service';
import { PageLink } from '@shared/models/page/page-link';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
@Component({
selector: 'tb-resource-autocomplete',
templateUrl: './resource-autocomplete.component.html',
styleUrls: [],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ResourceAutocompleteComponent),
multi: true
}]
})
export class ResourceAutocompleteComponent implements ControlValueAccessor, OnInit {
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
@coerceBoolean()
required: boolean;
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
placeholder: string;
@Input()
@coerceBoolean()
hideRequiredMarker = false;
@Input()
@coerceBoolean()
allowAutocomplete = false;
resourceFormGroup = this.fb.group({
resource: [null]
});
filteredResources$: Observable<Array<ResourceInfo>>;
searchText = '';
@ViewChild('resourceInput', {static: true}) resourceInput: ElementRef;
private modelValue: string | TbResourceId;
private dirty = false;
private propagateChange = (v: any) => { };
constructor(private fb: FormBuilder,
private resourceService: ResourceService) {
}
ngOnInit(): void {
if(this.required) {
this.resourceFormGroup.get('resource').setValidators(Validators.required);
this.resourceFormGroup.get('resource').updateValueAndValidity({emitEvent: false});
}
this.filteredResources$ = this.resourceFormGroup.get('resource').valueChanges
.pipe(
debounceTime(150),
tap(value => {
let modelValue;
if (isObject(value)) {
modelValue = value.id;
} else if (isEmptyStr(value)) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
if (value === null) {
this.clear();
}
}),
map(value => value ? (typeof value === 'string' ? value : value.title) : ''),
switchMap(name => this.fetchResources(name) ),
share()
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
if (this.disabled) {
this.resourceFormGroup.disable({emitEvent: false});
} else {
this.resourceFormGroup.enable({emitEvent: false});
}
}
writeValue(value: string | TbResourceId) {
if (isDefinedAndNotNull(value)) {
this.searchText = '';
if (isObject(value) && typeof value !== 'string' && (value as TbResourceId).id) {
this.resourceService.getResourceInfo(value.id, {ignoreLoading: true, ignoreErrors: true}).subscribe({
next: resource => {
this.modelValue = resource.id;
this.resourceFormGroup.get('resource').patchValue(resource, {emitEvent: false});
},
error: () => {
this.modelValue = '';
this.resourceFormGroup.get('resource').patchValue('');
}
});
} else {
this.modelValue = value;
this.resourceFormGroup.get('resource').patchValue(value, {emitEvent: false});
}
this.dirty = true;
}
}
displayResourceFn(resource?: ResourceInfo | string): string {
return isObject(resource) ? (resource as ResourceInfo).title : resource as string;
}
clear() {
this.resourceFormGroup.get('resource').patchValue('', {emitEvent: true});
setTimeout(() => {
this.resourceInput.nativeElement.blur();
this.resourceInput.nativeElement.focus();
}, 0);
}
onFocus() {
if (this.dirty) {
this.resourceFormGroup.get('resource').updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
private updateView(value: string | TbResourceId ) {
if (!isEqual(this.modelValue, value)) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
private fetchResources(searchText?: string): Observable<Array<ResourceInfo>> {
this.searchText = searchText;
return this.resourceService.getResources(new PageLink(50, 0, searchText), ResourceType.JS_MODULE, {ignoreLoading: true}).pipe(
catchError(() => of(null)),
map(data => data.data)
);
}
}

19
ui-ngx/src/app/shared/models/resource.models.ts

@ -21,14 +21,16 @@ import { TbResourceId } from '@shared/models/id/tb-resource-id';
export enum ResourceType {
LWM2M_MODEL = 'LWM2M_MODEL',
PKCS_12 = 'PKCS_12',
JKS = 'JKS'
JKS = 'JKS',
JS_MODULE = 'JS_MODULE'
}
export const ResourceTypeMIMETypes = new Map<ResourceType, string>(
[
[ResourceType.LWM2M_MODEL, 'application/xml,text/xml'],
[ResourceType.PKCS_12, 'application/x-pkcs12'],
[ResourceType.JKS, 'application/x-java-keystore']
[ResourceType.JKS, 'application/x-java-keystore'],
[ResourceType.JS_MODULE, 'text/javascript,application/javascript']
]
);
@ -36,19 +38,21 @@ export const ResourceTypeExtension = new Map<ResourceType, string>(
[
[ResourceType.LWM2M_MODEL, 'xml'],
[ResourceType.PKCS_12, 'p12,pfx'],
[ResourceType.JKS, 'jks']
[ResourceType.JKS, 'jks'],
[ResourceType.JS_MODULE, 'js']
]
);
export const ResourceTypeTranslationMap = new Map<ResourceType, string>(
[
[ResourceType.LWM2M_MODEL, 'LWM2M model'],
[ResourceType.PKCS_12, 'PKCS #12'],
[ResourceType.JKS, 'JKS']
[ResourceType.LWM2M_MODEL, 'resource.type.lwm2m-model'],
[ResourceType.PKCS_12, 'resource.type.pkcs-12'],
[ResourceType.JKS, 'resource.type.jks'],
[ResourceType.JS_MODULE, 'resource.type.js-module']
]
);
export interface ResourceInfo extends BaseData<TbResourceId> {
export interface ResourceInfo extends Omit<BaseData<TbResourceId>, 'name' | 'label'> {
tenantId?: TenantId;
resourceKey?: string;
title?: string;
@ -58,6 +62,7 @@ export interface ResourceInfo extends BaseData<TbResourceId> {
export interface Resource extends ResourceInfo {
data: string;
fileName: string;
name?: string;
}
export interface Resources extends ResourceInfo {

3
ui-ngx/src/app/shared/shared.module.ts

@ -189,6 +189,7 @@ import {
GtMdLgShowHideDirective
} from '@shared/layout/layout.directives';
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
import { ResourceAutocompleteComponent } from '@shared/components/resource/resource-autocomplete.component';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent } from '@shared/components/toggle-header.component';
import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
@ -361,6 +362,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ResourceAutocompleteComponent,
ToggleHeaderComponent,
RuleChainSelectComponent
],
@ -588,6 +590,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ResourceAutocompleteComponent,
ToggleHeaderComponent,
RuleChainSelectComponent
]

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

@ -3253,6 +3253,7 @@
},
"resource": {
"add": "Add Resource",
"all-types": "All",
"copyId": "Copy resource Id",
"delete": "Delete resource",
"delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.",
@ -3278,7 +3279,13 @@
"system": "System",
"title": "Title",
"title-required": "Title is required.",
"title-max-length": "Title should be less than 256"
"title-max-length": "Title should be less than 256",
"type": {
"jks": "JKS",
"js-module": "JS module",
"lwm2m-model": "LWM2M model",
"pkcs-12": "PKCS #12"
}
},
"rulechain": {
"rulechain": "Rule chain",

16
ui-ngx/yarn.lock

@ -3286,10 +3286,10 @@
dependencies:
"@types/react" "*"
"@types/systemjs@6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-6.1.1.tgz#eae17f2a080e867d01a2dd614f524ab227cf5a41"
integrity sha512-d1M6eDKBGWx7RbYy295VEFoOF9YDJkPI959QYnmzcmeaV+SP4D0xV7dEh3sN5XF3GvO3PhGzm+17Z598nvHQuQ==
"@types/systemjs@6.13.1":
version "6.13.1"
resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-6.13.1.tgz#fccf8049fdf328bca4cfbad3a9cc7bf088b45048"
integrity sha512-Jxo2/uif1WpkabfyvWpFmPWFPDdwKUmyL7xWzjtxNALEu2pgce+eISjbf0Vr+SsK/D9savO5kTRcf+COLK5eiQ==
"@types/tinycolor2@^1.4.3":
version "1.4.3"
@ -10101,10 +10101,10 @@ symbol-observable@4.0.0:
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205"
integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==
systemjs@6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.11.0.tgz#8df8e74fc05822e6c40170aa409b9ca64833315f"
integrity sha512-7YPIY44j+BoY+E6cGBSw0oCU8SNTTIHKZgftcBdwWkDzs/M86Fdlr21FrzAyph7Zo8r3CFGscyFe4rrBtixrBg==
systemjs@6.14.1:
version "6.14.1"
resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-6.14.1.tgz#95a580b91b50d0d69ff178ed4816f0ddbcea23c1"
integrity sha512-8ftwWd+XnQtZ/aGbatrN4QFNGrKJzmbtixW+ODpci7pyoTajg4sonPP8aFLESAcuVxaC1FyDESt+SpfFCH9rZQ==
table-layout@^1.0.2:
version "1.0.2"

Loading…
Cancel
Save