Browse Source

Widget Bundles to Wudget Types Many to Many support for UI

feature/widget-bundles
Igor Kulikov 3 years ago
parent
commit
875c8d526b
  1. 32
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  2. 17
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  3. 6
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java
  4. 2
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java
  5. 16
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java
  7. 8
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  8. 55
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  9. 2
      common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java
  10. 11
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java
  11. 26
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeIdFqnEntity.java
  12. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  13. 5
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  14. 127
      ui-ngx/src/app/core/http/widget.service.ts
  15. 42
      ui-ngx/src/app/core/services/menu.service.ts
  16. 4
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  17. 5
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  18. 5
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  19. 46
      ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.html
  20. 67
      ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.ts
  21. 3
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  22. 183
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  23. 1
      ui-ngx/src/app/modules/home/components/router-tabs.component.scss
  24. 3
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  25. 11
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  26. 6
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  27. 56
      ui-ngx/src/app/modules/home/pages/widget/move-widget-type-dialog.component.html
  28. 82
      ui-ngx/src/app/modules/home/pages/widget/move-widget-type-dialog.component.ts
  29. 5
      ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html
  30. 22
      ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts
  31. 17
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  32. 68
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  33. 181
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  34. 42
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html
  35. 205
      ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts
  36. 13
      ui-ngx/src/app/modules/home/pages/widget/widget-library.module.ts
  37. 59
      ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.html
  38. 49
      ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.scss
  39. 227
      ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.ts
  40. 23
      ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.html
  41. 43
      ui-ngx/src/app/modules/home/pages/widget/widget-type-tabs.component.ts
  42. 66
      ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html
  43. 68
      ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts
  44. 232
      ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts
  45. 134
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html
  46. 184
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.scss
  47. 178
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts
  48. 4
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts
  49. 3
      ui-ngx/src/app/shared/models/constants.ts
  50. 19
      ui-ngx/src/app/shared/models/entity-type.models.ts
  51. 5
      ui-ngx/src/app/shared/models/widget.models.ts
  52. 34
      ui-ngx/src/assets/locale/locale.constant-en_US.json

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

@ -74,6 +74,7 @@ public class WidgetTypeController extends AutoCommitController {
"Those properties are useful to edit the Widget Type but they are not required for Dashboard rendering. "; "Those properties are useful to edit the Widget Type but they are not required for Dashboard rendering. ";
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 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 TENANT_ONLY_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether only tenant widget types should be returned";
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";
@ApiOperation(value = "Get Widget Type Details (getWidgetTypeById)", @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) notes = "Get the Widget Type Details based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@ -88,6 +89,19 @@ public class WidgetTypeController extends AutoCommitController {
return checkWidgetTypeId(widgetTypeId, Operation.READ); return checkWidgetTypeId(widgetTypeId, Operation.READ);
} }
@ApiOperation(value = "Get Widget Type Info (getWidgetTypeInfoById)",
notes = "Get the Widget Type Info based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypeInfo/{widgetTypeId}", method = RequestMethod.GET)
@ResponseBody
public WidgetTypeInfo getWidgetTypeInfoById(
@ApiParam(value = WIDGET_TYPE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException {
checkParameter("widgetTypeId", strWidgetTypeId);
WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId));
return new WidgetTypeInfo(checkWidgetTypeId(widgetTypeId, Operation.READ));
}
@ApiOperation(value = "Create Or Update Widget Type (saveWidgetType)", @ApiOperation(value = "Create Or Update Widget Type (saveWidgetType)",
notes = "Create or update the Widget Type. " + WIDGET_TYPE_DESCRIPTION + " " + notes = "Create or update the Widget Type. " + WIDGET_TYPE_DESCRIPTION + " " +
"When creating the Widget Type, platform generates Widget Type Id as " + UUID_WIKI_LINK + "When creating the Widget Type, platform generates Widget Type Id as " + UUID_WIKI_LINK +
@ -103,7 +117,9 @@ public class WidgetTypeController extends AutoCommitController {
@ResponseBody @ResponseBody
public WidgetTypeDetails saveWidgetType( public WidgetTypeDetails saveWidgetType(
@ApiParam(value = "A JSON value representing the Widget Type Details.", required = true) @ApiParam(value = "A JSON value representing the Widget Type Details.", required = true)
@RequestBody WidgetTypeDetails widgetTypeDetails) throws Exception { @RequestBody WidgetTypeDetails widgetTypeDetails,
@ApiParam(value = UPDATE_EXISTING_BY_FQN_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean updateExistingByFqn) throws Exception {
var currentUser = getCurrentUser(); var currentUser = getCurrentUser();
if (Authority.SYS_ADMIN.equals(currentUser.getAuthority())) { if (Authority.SYS_ADMIN.equals(currentUser.getAuthority())) {
widgetTypeDetails.setTenantId(TenantId.SYS_TENANT_ID); widgetTypeDetails.setTenantId(TenantId.SYS_TENANT_ID);
@ -112,7 +128,7 @@ public class WidgetTypeController extends AutoCommitController {
} }
checkEntity(widgetTypeDetails.getId(), widgetTypeDetails, Resource.WIDGET_TYPE); checkEntity(widgetTypeDetails.getId(), widgetTypeDetails, Resource.WIDGET_TYPE);
return tbWidgetTypeService.save(widgetTypeDetails, currentUser); return tbWidgetTypeService.save(widgetTypeDetails, updateExistingByFqn != null && updateExistingByFqn, currentUser);
} }
@ApiOperation(value = "Delete widget type (deleteWidgetType)", @ApiOperation(value = "Delete widget type (deleteWidgetType)",
@ -224,6 +240,18 @@ public class WidgetTypeController extends AutoCommitController {
return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId)); return checkNotNull(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(getTenantId(), widgetsBundleId));
} }
@ApiOperation(value = "Get all Widget type fqns for specified Bundle (getBundleWidgetTypeFqns)",
notes = "Returns an array of Widget Type fqns that belong to specified Widget Bundle." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypeFqns", params = {"widgetsBundleId"}, method = RequestMethod.GET)
@ResponseBody
public List<String> getBundleWidgetTypeFqns(
@ApiParam(value = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
return checkNotNull(widgetTypeService.findWidgetFqnsByWidgetsBundleId(getTenantId(), widgetsBundleId));
}
@ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)", @ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfosByBundleAlias) (Deprecated)",
notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER) 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')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")

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

@ -136,6 +136,23 @@ public class WidgetsBundleController extends BaseController {
tbWidgetsBundleService.updateWidgetsBundleWidgetTypes(widgetsBundleId, new ArrayList<>(widgetTypeIds), currentUser); tbWidgetsBundleService.updateWidgetsBundleWidgetTypes(widgetsBundleId, new ArrayList<>(widgetTypeIds), currentUser);
} }
@ApiOperation(value = "Update widgets bundle widgets list from widget type FQNs list (updateWidgetsBundleWidgetFqns)",
notes = "Updates widgets bundle widgets list from widget type FQNs list." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetsBundle/{widgetsBundleId}/widgetTypeFqns", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void updateWidgetsBundleWidgetFqns(
@ApiParam(value = WIDGET_BUNDLE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("widgetsBundleId") String strWidgetsBundleId,
@ApiParam(value = "Ordered list of widget type FQNs to be included by widgets bundle")
@RequestBody List<String> widgetTypeFqns) throws Exception {
checkParameter("widgetsBundleId", strWidgetsBundleId);
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
checkNotNull(widgetTypeFqns);
var currentUser = getCurrentUser();
tbWidgetsBundleService.updateWidgetsBundleWidgetFqns(widgetsBundleId, widgetTypeFqns, currentUser);
}
@ApiOperation(value = "Delete widgets bundle (deleteWidgetsBundle)", @ApiOperation(value = "Delete widgets bundle (deleteWidgetsBundle)",
notes = "Deletes the widget bundle. Referencing non-existing Widget Bundle Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) notes = "Deletes the widget bundle. Referencing non-existing Widget Bundle Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")

6
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/DefaultWidgetsBundleService.java

@ -73,4 +73,10 @@ public class DefaultWidgetsBundleService extends AbstractTbEntityService impleme
widgetTypeService.updateWidgetsBundleWidgetTypes(user.getTenantId(), widgetsBundleId, widgetTypeIds); widgetTypeService.updateWidgetsBundleWidgetTypes(user.getTenantId(), widgetsBundleId, widgetTypeIds);
autoCommit(user, widgetsBundleId); autoCommit(user, widgetsBundleId);
} }
@Override
public void updateWidgetsBundleWidgetFqns(WidgetsBundleId widgetsBundleId, List<String> widgetFqns, User user) throws Exception {
widgetTypeService.updateWidgetsBundleWidgetFqns(user.getTenantId(), widgetsBundleId, widgetFqns);
autoCommit(user, widgetsBundleId);
}
} }

2
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/bundle/TbWidgetsBundleService.java

@ -27,4 +27,6 @@ public interface TbWidgetsBundleService extends SimpleTbEntityService<WidgetsBun
void updateWidgetsBundleWidgetTypes(WidgetsBundleId widgetsBundleId, List<WidgetTypeId> widgetTypeIds, User user) throws Exception; void updateWidgetsBundleWidgetTypes(WidgetsBundleId widgetsBundleId, List<WidgetTypeId> widgetTypeIds, User user) throws Exception;
void updateWidgetsBundleWidgetFqns(WidgetsBundleId widgetsBundleId, List<String> widgetFqns, User user) throws Exception;
} }

16
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/DefaultWidgetTypeService.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -34,9 +35,20 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements
private final WidgetTypeService widgetTypeService; private final WidgetTypeService widgetTypeService;
@Override @Override
public WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, User user) throws Exception { public WidgetTypeDetails save(WidgetTypeDetails entity, User user) throws Exception {
ActionType actionType = widgetTypeDetails.getId() == null ? ActionType.ADDED : ActionType.UPDATED; return this.save(entity, false, user);
}
@Override
public WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, User user) throws Exception {
TenantId tenantId = widgetTypeDetails.getTenantId(); TenantId tenantId = widgetTypeDetails.getTenantId();
if (widgetTypeDetails.getId() == null && updateExistingByFqn) {
WidgetType widgetType = widgetTypeService.findWidgetTypeByTenantIdAndFqn(tenantId, widgetTypeDetails.getFqn());
if (widgetType != null) {
widgetTypeDetails.setId(widgetType.getId());
}
}
ActionType actionType = widgetTypeDetails.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
try { try {
WidgetTypeDetails savedWidgetTypeDetails = checkNotNull(widgetTypeService.saveWidgetType(widgetTypeDetails)); WidgetTypeDetails savedWidgetTypeDetails = checkNotNull(widgetTypeService.saveWidgetType(widgetTypeDetails));
autoCommit(user, savedWidgetTypeDetails.getId()); autoCommit(user, savedWidgetTypeDetails.getId());

4
application/src/main/java/org/thingsboard/server/service/entitiy/widgets/type/TbWidgetTypeService.java

@ -15,8 +15,12 @@
*/ */
package org.thingsboard.server.service.entitiy.widgets.type; package org.thingsboard.server.service.entitiy.widgets.type;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService; import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
public interface TbWidgetTypeService extends SimpleTbEntityService<WidgetTypeDetails> { public interface TbWidgetTypeService extends SimpleTbEntityService<WidgetTypeDetails> {
WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, User user) throws Exception;
} }

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

@ -99,6 +99,7 @@ import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService;
@ -136,6 +137,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired @Autowired
private AdminSettingsService adminSettingsService; private AdminSettingsService adminSettingsService;
@Autowired
private WidgetTypeService widgetTypeService;
@Autowired @Autowired
private WidgetsBundleService widgetsBundleService; private WidgetsBundleService widgetsBundleService;
@ -485,6 +489,10 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
public void deleteSystemWidgetBundle(String bundleAlias) throws Exception { public void deleteSystemWidgetBundle(String bundleAlias) throws Exception {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias); WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) { 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());
}
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId()); widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
} }
} }

55
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@ -46,7 +47,9 @@ import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
@ -70,6 +73,7 @@ public class InstallScripts {
public static final String DEVICE_PROFILE_DIR = "device_profile"; public static final String DEVICE_PROFILE_DIR = "device_profile";
public static final String DEMO_DIR = "demo"; public static final String DEMO_DIR = "demo";
public static final String RULE_CHAINS_DIR = "rule_chains"; public static final String RULE_CHAINS_DIR = "rule_chains";
public static final String WIDGET_TYPES_DIR = "widget_types";
public static final String WIDGET_BUNDLES_DIR = "widget_bundles"; public static final String WIDGET_BUNDLES_DIR = "widget_bundles";
public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates"; public static final String OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
public static final String DASHBOARDS_DIR = "dashboards"; public static final String DASHBOARDS_DIR = "dashboards";
@ -180,6 +184,23 @@ public class InstallScripts {
} }
public void loadSystemWidgets() throws Exception { public void loadSystemWidgets() throws Exception {
Path widgetTypesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR);
if (Files.exists(widgetTypesDir)) {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetTypesDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach(
path -> {
try {
JsonNode widgetTypeJson = JacksonUtil.toJsonNode(path.toFile());
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
widgetTypeService.saveWidgetType(widgetTypeDetails);
} catch (Exception e) {
log.error("Unable to load widget type from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widget type from json", e);
}
}
);
}
}
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR); Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) { try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach( dirStream.forEach(
@ -189,19 +210,29 @@ public class InstallScripts {
JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle"); JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class); WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class);
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes"); List<String> widgetTypeFqns = new ArrayList<>();
widgetTypesArrayJson.forEach( if (widgetsBundleDescriptorJson.has("widgetTypes")) {
widgetTypeJson -> { JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
try { widgetTypesArrayJson.forEach(
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class); widgetTypeJson -> {
// widgetTypeDetails.setBundleAlias(savedWidgetsBundle.getAlias()); // TODO: try {
widgetTypeService.saveWidgetType(widgetTypeDetails); WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
} catch (Exception e) { var savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails);
log.error("Unable to load widget type from json: [{}]", path.toString()); widgetTypeFqns.add(savedWidgetType.getFqn());
throw new RuntimeException("Unable to load widget type from json", e); } catch (Exception e) {
log.error("Unable to load widget type from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widget type from json", e);
}
} }
} );
); }
if (widgetsBundleDescriptorJson.has("widgetTypeFqns")) {
JsonNode widgetFqnsArrayJson = widgetsBundleDescriptorJson.get("widgetTypeFqns");
widgetFqnsArrayJson.forEach(fqnJson -> {
widgetTypeFqns.add(fqnJson.asText());
});
}
widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, savedWidgetsBundle.getId(), widgetTypeFqns);
} catch (Exception e) { } catch (Exception e) {
log.error("Unable to load widgets bundle from json: [{}]", path.toString()); log.error("Unable to load widgets bundle from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widgets bundle from json", e); throw new RuntimeException("Unable to load widgets bundle from json", e);

2
common/data/src/main/java/org/thingsboard/server/common/data/sync/JsonTbEntity.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundle;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
@ -52,6 +53,7 @@ import java.lang.annotation.Target;
@Type(name = "CUSTOMER", value = Customer.class), @Type(name = "CUSTOMER", value = Customer.class),
@Type(name = "ENTITY_VIEW", value = EntityView.class), @Type(name = "ENTITY_VIEW", value = EntityView.class),
@Type(name = "WIDGETS_BUNDLE", value = WidgetsBundle.class), @Type(name = "WIDGETS_BUNDLE", value = WidgetsBundle.class),
@Type(name = "WIDGET_TYPE", value = WidgetTypeDetails.class),
@Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class), @Type(name = "NOTIFICATION_TEMPLATE", value = NotificationTemplate.class),
@Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class), @Type(name = "NOTIFICATION_TARGET", value = NotificationTarget.class),
@Type(name = "NOTIFICATION_RULE", value = NotificationRule.class) @Type(name = "NOTIFICATION_RULE", value = NotificationRule.class)

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

@ -50,4 +50,15 @@ public class WidgetTypeInfo extends BaseWidgetType {
this.description = widgetTypeInfo.getDescription(); this.description = widgetTypeInfo.getDescription();
this.widgetType = widgetTypeInfo.getWidgetType(); this.widgetType = widgetTypeInfo.getWidgetType();
} }
public WidgetTypeInfo(WidgetTypeDetails widgetTypeDetails) {
super(widgetTypeDetails);
this.image = widgetTypeDetails.getImage();
this.description = widgetTypeDetails.getDescription();
if (widgetTypeDetails.getDescriptor() != null && widgetTypeDetails.getDescriptor().has("type")) {
this.widgetType = widgetTypeDetails.getDescriptor().get("type").asText();
} else {
this.widgetType = "";
}
}
} }

26
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.scss → dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeIdFqnEntity.java

@ -13,20 +13,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host { package org.thingsboard.server.dao.model.sql;
button.tb-add-new-widget {
height: auto; import lombok.AllArgsConstructor;
padding-right: 12px; import lombok.Data;
font-size: 24px;
border-style: dashed; import java.util.UUID;
border-width: 2px;
}
}
:host ::ng-deep { @Data
.tb-widget-library { @AllArgsConstructor
.tb-widget-container { public class WidgetTypeIdFqnEntity {
cursor: pointer; private UUID id;
} private String fqn;
}
} }

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

@ -35,6 +35,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao; import org.thingsboard.server.dao.util.SqlDao;
import org.thingsboard.server.dao.widget.WidgetTypeDao; import org.thingsboard.server.dao.widget.WidgetTypeDao;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -139,8 +140,10 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
@Override @Override
public List<WidgetTypeId> findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List<String> widgetFqns) { public List<WidgetTypeId> findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List<String> widgetFqns) {
return widgetTypeRepository.findWidgetTypeIdsByTenantIdAndFqns(tenantId, widgetFqns).stream() var idFqnPairs = widgetTypeRepository.findWidgetTypeIdsByTenantIdAndFqns(tenantId, widgetFqns);
.map(WidgetTypeId::new).collect(Collectors.toList()); idFqnPairs.sort(Comparator.comparingInt(o -> widgetFqns.indexOf(o.getFqn())));
return idFqnPairs.stream()
.map(id -> new WidgetTypeId(id.getId())).collect(Collectors.toList());
} }
@Override @Override

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

@ -23,6 +23,7 @@ import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.WidgetTypeDetailsEntity; import org.thingsboard.server.dao.model.sql.WidgetTypeDetailsEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeEntity; 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.WidgetTypeInfoEntity;
import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity; import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity;
@ -81,10 +82,10 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
"AND wbw.widgetTypeId = wtd.id ORDER BY wbw.widgetTypeOrder") "AND wbw.widgetTypeId = wtd.id ORDER BY wbw.widgetTypeOrder")
List<String> findWidgetFqnsByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId); List<String> findWidgetFqnsByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId);
@Query("SELECT wtd.id FROM WidgetTypeDetailsEntity wtd " + @Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeIdFqnEntity(wtd.id, wtd.fqn) FROM WidgetTypeDetailsEntity wtd " +
"WHERE wtd.tenantId = :tenantId " + "WHERE wtd.tenantId = :tenantId " +
"AND wtd.fqn IN (:widgetFqns)") "AND wtd.fqn IN (:widgetFqns)")
List<UUID> findWidgetTypeIdsByTenantIdAndFqns(@Param("tenantId") UUID tenantId, @Param("widgetFqns") List<String> widgetFqns); List<WidgetTypeIdFqnEntity> findWidgetTypeIdsByTenantIdAndFqns(@Param("tenantId") UUID tenantId, @Param("widgetFqns") List<String> widgetFqns);
@Query("SELECT wt FROM WidgetTypeEntity wt " + @Query("SELECT wt FROM WidgetTypeEntity wt " +
"WHERE wt.tenantId = :tenantId AND wt.fqn = :fqn") "WHERE wt.tenantId = :tenantId AND wt.fqn = :fqn")

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

@ -22,6 +22,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { import {
BaseWidgetType,
fullWidgetTypeFqn, fullWidgetTypeFqn,
Widget, Widget,
WidgetType, WidgetType,
@ -30,9 +31,7 @@ import {
WidgetTypeInfo, WidgetTypeInfo,
widgetTypesData widgetTypesData
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ResourcesService } from '../services/resources.service';
import { toWidgetInfo, toWidgetTypeDetails, WidgetInfo } from '@app/modules/home/models/widget-component.models'; import { toWidgetInfo, toWidgetTypeDetails, WidgetInfo } from '@app/modules/home/models/widget-component.models';
import { filter, map, mergeMap, tap } from 'rxjs/operators'; import { filter, map, mergeMap, tap } from 'rxjs/operators';
import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { WidgetTypeId } from '@shared/models/id/widget-type-id';
@ -56,8 +55,6 @@ export class WidgetService {
constructor( constructor(
private http: HttpClient, private http: HttpClient,
private utils: UtilsService,
private resources: ResourcesService,
private translate: TranslateService, private translate: TranslateService,
private router: Router private router: Router
) { ) {
@ -110,46 +107,70 @@ 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);
})
);
}
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);
})
);
}
public deleteWidgetsBundle(widgetsBundleId: string, config?: RequestConfig) { public deleteWidgetsBundle(widgetsBundleId: string, config?: RequestConfig) {
return this.getWidgetsBundle(widgetsBundleId, config).pipe( return this.getWidgetsBundle(widgetsBundleId, config).pipe(
mergeMap((widgetsBundle) => this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`, mergeMap((widgetsBundle) => this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`,
defaultHttpOptionsFromConfig(config)).pipe( defaultHttpOptionsFromConfig(config)).pipe(
tap(() => { tap(() => {
this.invalidateWidgetsBundleCache(); this.invalidateWidgetsBundleCache();
this.widgetsBundleDeleted(widgetsBundle);
}) })
) )
)); ));
} }
public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean, public getBundleWidgetTypes(widgetsBundleId: string,
config?: RequestConfig): Observable<Array<WidgetType>> { config?: RequestConfig): Observable<Array<WidgetType>> {
return this.http.get<Array<WidgetType>>(`/api/widgetTypes?isSystem=${isSystem}&bundleAlias=${bundleAlias}`, return this.http.get<Array<WidgetType>>(`/api/widgetTypes?widgetsBundleId=${widgetsBundleId}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getBundleWidgetTypesDetails(bundleAlias: string, isSystem: boolean, public getBundleWidgetTypesDetails(widgetsBundleId: string,
config?: RequestConfig): Observable<Array<WidgetTypeDetails>> { config?: RequestConfig): Observable<Array<WidgetTypeDetails>> {
return this.http.get<Array<WidgetTypeDetails>>(`/api/widgetTypesDetails?isSystem=${isSystem}&bundleAlias=${bundleAlias}`, return this.http.get<Array<WidgetTypeDetails>>(`/api/widgetTypesDetails?widgetsBundleId=${widgetsBundleId}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
public getBundleWidgetTypeInfos(bundleAlias: string, isSystem: boolean, public getBundleWidgetTypeFqns(widgetsBundleId: string,
config?: RequestConfig): Observable<Array<string>> {
return this.http.get<Array<string>>(`/api/widgetTypeFqns?widgetsBundleId=${widgetsBundleId}`,
defaultHttpOptionsFromConfig(config));
}
public getBundleWidgetTypeInfos(widgetsBundleId: string,
config?: RequestConfig): Observable<Array<WidgetTypeInfo>> { config?: RequestConfig): Observable<Array<WidgetTypeInfo>> {
const key = bundleAlias + (isSystem ? '_sys' : ''); if (this.widgetTypeInfosCache.has(widgetsBundleId)) {
if (this.widgetTypeInfosCache.has(key)) { return of(this.widgetTypeInfosCache.get(widgetsBundleId));
return of(this.widgetTypeInfosCache.get(key));
} else { } else {
return this.http.get<Array<WidgetTypeInfo>>(`/api/widgetTypesInfos?isSystem=${isSystem}&bundleAlias=${bundleAlias}`, return this.http.get<Array<WidgetTypeInfo>>(`/api/widgetTypesInfos?widgetsBundleId=${widgetsBundleId}`,
defaultHttpOptionsFromConfig(config)).pipe( defaultHttpOptionsFromConfig(config)).pipe(
tap((res) => this.widgetTypeInfosCache.set(key, res) ) tap((res) => this.widgetTypeInfosCache.set(widgetsBundleId, res) )
); );
} }
} }
public loadBundleLibraryWidgets(bundleAlias: string, isSystem: boolean, public loadBundleLibraryWidgets(widgetsBundleId: string,
config?: RequestConfig): Observable<Array<Widget>> { config?: RequestConfig): Observable<Array<Widget>> {
return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe( return this.getBundleWidgetTypes(widgetsBundleId, config).pipe(
map((types) => { map((types) => {
types = types.sort((a, b) => { types = types.sort((a, b) => {
let result = (a.deprecated ? 1 : 0) - (b.deprecated ? 1 : 0); let result = (a.deprecated ? 1 : 0) - (b.deprecated ? 1 : 0);
@ -211,10 +232,9 @@ export class WidgetService {
public saveWidgetTypeDetails(widgetInfo: WidgetInfo, public saveWidgetTypeDetails(widgetInfo: WidgetInfo,
id: WidgetTypeId, id: WidgetTypeId,
bundleAlias: string,
createdTime: number, createdTime: number,
config?: RequestConfig): Observable<WidgetTypeDetails> { config?: RequestConfig): Observable<WidgetTypeDetails> {
const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, bundleAlias, createdTime); const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, createdTime);
return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails, return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails,
defaultHttpOptionsFromConfig(config)).pipe( defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => { tap((savedWidgetType) => {
@ -222,46 +242,47 @@ export class WidgetService {
})); }));
} }
public setWidgetTypeDeprecated(widgetTypeId: string, deprecated: boolean, config?: RequestConfig): Observable<WidgetTypeDetails> { public saveImportedWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails,
return this.http.post<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}/deprecate/${deprecated}`, config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>('/api/widgetType?updateExistingByFqn=true', widgetTypeDetails,
defaultHttpOptionsFromConfig(config)).pipe( defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => { tap((savedWidgetType) => {
this.widgetTypeUpdated(savedWidgetType); this.widgetTypeUpdated(savedWidgetType);
})); }));
} }
public moveWidgetType(widgetTypeId: string, targetBundleAlias: string, config?: RequestConfig): Observable<WidgetTypeDetails> { public getWidgetTypeById(widgetTypeId: string,
return this.http.post<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}/move?targetBundleAlias=${targetBundleAlias}`, config?: RequestConfig): Observable<WidgetTypeDetails> {
defaultHttpOptionsFromConfig(config)).pipe( return this.http.get<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}`,
tap((savedWidgetType) => { defaultHttpOptionsFromConfig(config));
this.widgetTypeUpdated(savedWidgetType);
}));
} }
public saveImportedWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails, public getWidgetTypeInfoById(widgetTypeId: string,
config?: RequestConfig): Observable<WidgetTypeDetails> { config?: RequestConfig): Observable<WidgetTypeInfo> {
return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails, return this.http.get<WidgetTypeInfo>(`/api/widgetTypeInfo/${widgetTypeId}`,
defaultHttpOptionsFromConfig(config)).pipe( defaultHttpOptionsFromConfig(config));
tap((savedWidgetType) => { }
this.widgetTypeUpdated(savedWidgetType);
})); public saveWidgetType(widgetTypeDetails: WidgetTypeDetails,
config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>(`/api/widgetType`,
defaultHttpOptionsFromConfig(config));
} }
public deleteWidgetType(fullFqn: string, public deleteWidgetType(widgetTypeId: string,
config?: RequestConfig) { config?: RequestConfig) {
return this.getWidgetType(fullFqn, config).pipe( return this.getWidgetTypeById(widgetTypeId, config).pipe(
mergeMap((widgetTypeInstance) => this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`, mergeMap((widgetTypeDetails) =>
defaultHttpOptionsFromConfig(config)).pipe( this.http.delete(`/api/widgetType/${widgetTypeId}`, defaultHttpOptionsFromConfig(config)).pipe(
tap(() => { tap(() => {
this.widgetTypeUpdated(widgetTypeInstance); this.widgetTypeUpdated(widgetTypeDetails);
}) })
) )
)); ));
} }
public getWidgetTypeById(widgetTypeId: string, public getWidgetTypes(pageLink: PageLink, tenantOnly = false, config?: RequestConfig): Observable<PageData<WidgetTypeInfo>> {
config?: RequestConfig): Observable<WidgetTypeDetails> { return this.http.get<PageData<WidgetTypeInfo>>(`/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}`,
return this.http.get<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}`,
defaultHttpOptionsFromConfig(config)); defaultHttpOptionsFromConfig(config));
} }
@ -286,26 +307,14 @@ export class WidgetService {
this.widgetsInfoInMemoryCache.set(widgetInfo.fullFqn, widgetInfo); this.widgetsInfoInMemoryCache.set(widgetInfo.fullFqn, widgetInfo);
} }
private widgetTypeUpdated(updatedWidgetType: WidgetType): void { private widgetTypeUpdated(updatedWidgetType: BaseWidgetType): void {
this.deleteWidgetInfoFromCache(fullWidgetTypeFqn(updatedWidgetType)); this.deleteWidgetInfoFromCache(fullWidgetTypeFqn(updatedWidgetType));
} }
private widgetsBundleDeleted(widgetsBundle: WidgetsBundle): void {
this.deleteWidgetsBundleFromCache(widgetsBundle.alias);
}
public deleteWidgetInfoFromCache(fullFqn: string) { public deleteWidgetInfoFromCache(fullFqn: string) {
this.widgetsInfoInMemoryCache.delete(fullFqn); this.widgetsInfoInMemoryCache.delete(fullFqn);
} }
private deleteWidgetsBundleFromCache(bundleAlias: string) {
this.widgetsInfoInMemoryCache.forEach((widgetInfo, fullFqn) => {
if (widgetInfo.bundleAlias === bundleAlias) {
this.widgetsInfoInMemoryCache.delete(fullFqn);
}
});
}
private loadWidgetsBundleCache(config?: RequestConfig): Observable<any> { private loadWidgetsBundleCache(config?: RequestConfig): Observable<any> {
if (!this.allWidgetsBundles) { if (!this.allWidgetsBundles) {
if (!this.loadWidgetsBundleCacheSubject) { if (!this.loadWidgetsBundleCacheSubject) {

42
ui-ngx/src/app/core/services/menu.service.ts

@ -124,8 +124,24 @@ export class MenuService {
id: 'widget_library', id: 'widget_library',
name: 'widget.widget-library', name: 'widget.widget-library',
type: 'link', type: 'link',
path: '/resources/widgets-bundles', path: '/resources/widgets-library',
icon: 'now_widgets' icon: 'now_widgets',
pages: [
{
id: 'widget_types',
name: 'widget.widgets',
type: 'link',
path: '/resources/widgets-library/widget-types',
icon: 'now_widgets'
},
{
id: 'widgets_bundles',
name: 'widgets-bundle.widgets-bundles',
type: 'link',
path: '/resources/widgets-library/widgets-bundles',
icon: 'now_widgets'
}
]
}, },
{ {
id: 'resources_library', id: 'resources_library',
@ -283,7 +299,7 @@ export class MenuService {
{ {
name: 'widget.widget-library', name: 'widget.widget-library',
icon: 'now_widgets', icon: 'now_widgets',
path: '/widgets-bundles' path: '/resources/widgets-library',
} }
] ]
}, },
@ -492,8 +508,24 @@ export class MenuService {
id: 'widget_library', id: 'widget_library',
name: 'widget.widget-library', name: 'widget.widget-library',
type: 'link', type: 'link',
path: '/resources/widgets-bundles', path: '/resources/widgets-library',
icon: 'now_widgets' icon: 'now_widgets',
pages: [
{
id: 'widget_types',
name: 'widget.widgets',
type: 'link',
path: '/resources/widgets-library/widget-types',
icon: 'now_widgets'
},
{
id: 'widgets_bundles',
name: 'widgets-bundle.widgets-bundles',
type: 'link',
path: '/resources/widgets-library/widgets-bundles',
icon: 'now_widgets'
}
]
}, },
{ {
id: 'resources_library', id: 'resources_library',

4
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts

@ -567,9 +567,7 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
this.widgetsCarouselIndex = 0; this.widgetsCarouselIndex = 0;
if (widgetsBundle) { if (widgetsBundle) {
this.widgetsLoaded = false; this.widgetsLoaded = false;
const bundleAlias = widgetsBundle.alias; this.widgetService.getBundleWidgetTypes(widgetsBundle.id.id).subscribe(
const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe(
(widgetTypes) => { (widgetTypes) => {
widgetTypes = widgetTypes.sort((a, b) => { widgetTypes = widgetTypes.sort((a, b) => {
let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]); let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);

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

@ -121,12 +121,9 @@ export class DashboardWidgetSelectComponent implements OnInit {
private getWidgets(): Observable<Array<WidgetInfo>> { private getWidgets(): Observable<Array<WidgetInfo>> {
if (!this.widgetsInfo) { if (!this.widgetsInfo) {
if (this.widgetsBundle !== null) { if (this.widgetsBundle !== null) {
const bundleAlias = this.widgetsBundle.alias;
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
this.loadingWidgetsSubject.next(true); this.loadingWidgetsSubject.next(true);
this.widgetsInfo = this.widgetsService.getBundleWidgetTypeInfos(bundleAlias, isSystem).pipe( this.widgetsInfo = this.widgetsService.getBundleWidgetTypeInfos(this.widgetsBundle.id.id).pipe(
map(widgets => { map(widgets => {
widgets = widgets.sort((a, b) => b.createdTime - a.createdTime);
const widgetTypes = new Set<widgetType>(); const widgetTypes = new Set<widgetType>();
const hasDeprecated = widgets.some(w => w.deprecated); const hasDeprecated = widgets.some(w => w.deprecated);
const widgetInfos = widgets.map((widgetTypeInfo) => { const widgetInfos = widgets.map((widgetTypeInfo) => {

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

@ -178,6 +178,9 @@ import {
import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module'; import { WidgetConfigComponentsModule } from '@home/components/widget/config/widget-config-components.module';
import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component';
import {
ExportWidgetsBundleDialogComponent
} from '@home/components/import-export/export-widgets-bundle-dialog.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -228,6 +231,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
CustomDialogContainerComponent, CustomDialogContainerComponent,
ImportDialogComponent, ImportDialogComponent,
ImportDialogCsvComponent, ImportDialogCsvComponent,
ExportWidgetsBundleDialogComponent,
SelectTargetLayoutDialogComponent, SelectTargetLayoutDialogComponent,
SelectTargetStateDialogComponent, SelectTargetStateDialogComponent,
AddWidgetToDashboardDialogComponent, AddWidgetToDashboardDialogComponent,
@ -372,6 +376,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
CustomDialogContainerComponent, CustomDialogContainerComponent,
ImportDialogComponent, ImportDialogComponent,
ImportDialogCsvComponent, ImportDialogCsvComponent,
ExportWidgetsBundleDialogComponent,
TableColumnsAssignmentComponent, TableColumnsAssignmentComponent,
SelectTargetLayoutDialogComponent, SelectTargetLayoutDialogComponent,
SelectTargetStateDialogComponent, SelectTargetStateDialogComponent,

46
ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.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-toolbar color="primary">
<h2 translate>widgets-bundle.export</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<mat-checkbox [formControl]="exportWidgetsFormControl">{{ 'widgets-bundle.export-widgets-bundle-widgets-prompt' | translate }}</mat-checkbox>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="export()"
[disabled]="(isLoading$ | async)">
{{ 'action.export' | translate }}
</button>
</div>

67
ui-ngx/src/app/modules/home/components/import-export/export-widgets-bundle-dialog.component.ts

@ -0,0 +1,67 @@
///
/// 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, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormControl } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
export interface ExportWidgetsBundleDialogData {
widgetsBundle: WidgetsBundle;
}
export interface ExportWidgetsBundleDialogResult {
exportWidgets: boolean;
}
@Component({
selector: 'tb-export-widgets-bundle-dialog',
templateUrl: './export-widgets-bundle-dialog.component.html',
providers: [],
styleUrls: []
})
export class ExportWidgetsBundleDialogComponent extends DialogComponent<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogResult>
implements OnInit {
widgetsBundle: WidgetsBundle;
exportWidgetsFormControl = new FormControl(false);
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: ExportWidgetsBundleDialogData,
public dialogRef: MatDialogRef<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogResult>) {
super(store, router, dialogRef);
this.widgetsBundle = data.widgetsBundle;
}
ngOnInit(): void {
}
cancel(): void {
this.dialogRef.close(null);
}
export(): void {
this.dialogRef.close({
exportWidgets: this.exportWidgetsFormControl.value
});
}
}

3
ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts

@ -25,7 +25,8 @@ export interface ImportWidgetResult {
export interface WidgetsBundleItem { export interface WidgetsBundleItem {
widgetsBundle: WidgetsBundle; widgetsBundle: WidgetsBundle;
widgetTypes: WidgetTypeDetails[]; widgetTypes?: WidgetTypeDetails[];
widgetTypeFqns?: string[];
} }
export interface CsvToJsonConfig { export interface CsvToJsonConfig {

183
ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts

@ -34,8 +34,8 @@ import {
} from '@shared/models/alias.models'; } from '@shared/models/alias.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ImportDialogComponent, ImportDialogData } from '@home/components/import-export/import-dialog.component'; import { ImportDialogComponent, ImportDialogData } from '@home/components/import-export/import-dialog.component';
import { forkJoin, Observable, of } from 'rxjs'; import { forkJoin, Observable, of, Subject } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators'; import { catchError, map, mergeMap, switchMap, tap } from 'rxjs/operators';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { Widget, WidgetSize, WidgetType, WidgetTypeDetails } from '@shared/models/widget.models'; import { Widget, WidgetSize, WidgetType, WidgetTypeDetails } from '@shared/models/widget.models';
@ -58,7 +58,6 @@ import {
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models'; import { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models';
import { RequestConfig } from '@core/http/http-utils'; import { RequestConfig } from '@core/http/http-utils';
@ -75,6 +74,11 @@ import { EdgeService } from '@core/http/edge.service';
import { RuleNode } from '@shared/models/rule-node.models'; import { RuleNode } from '@shared/models/rule-node.models';
import { AssetProfileService } from '@core/http/asset-profile.service'; import { AssetProfileService } from '@core/http/asset-profile.service';
import { AssetProfile } from '@shared/models/asset.models'; import { AssetProfile } from '@shared/models/asset.models';
import {
ExportWidgetsBundleDialogComponent,
ExportWidgetsBundleDialogData,
ExportWidgetsBundleDialogResult
} from '@home/components/import-export/export-widgets-bundle-dialog.component';
// @dynamic // @dynamic
@Injectable() @Injectable()
@ -261,9 +265,6 @@ export class ImportExportService {
public exportWidgetType(widgetTypeId: string) { public exportWidgetType(widgetTypeId: string) {
this.widgetService.getWidgetTypeById(widgetTypeId).subscribe( this.widgetService.getWidgetTypeById(widgetTypeId).subscribe(
(widgetTypeDetails) => { (widgetTypeDetails) => {
if (isDefined(widgetTypeDetails.bundleAlias)) {
delete widgetTypeDetails.bundleAlias;
}
let name = widgetTypeDetails.name; let name = widgetTypeDetails.name;
name = name.toLowerCase().replace(/\W/g, '_'); name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareExport(widgetTypeDetails), name); this.exportToPc(this.prepareExport(widgetTypeDetails), name);
@ -274,7 +275,28 @@ export class ImportExportService {
); );
} }
public importWidgetType(bundleAlias: string): Observable<WidgetType> { public exportWidgetTypes(widgetTypeIds: string[]): Observable<void> {
const widgetTypesObservables: Array<Observable<WidgetTypeDetails>> = [];
for (const id of widgetTypeIds) {
widgetTypesObservables.push(this.widgetService.getWidgetTypeById(id));
}
return forkJoin(widgetTypesObservables).pipe(
map((widgetTypes) =>
Object.fromEntries(widgetTypes.map(wt=> {
let name = wt.name;
name = name.toLowerCase().replace(/\W/g, '_') + `.${JSON_TYPE.extension}`;
const data = JSON.stringify(this.prepareExport(wt), null, 2);
return [name, data];
}))),
mergeMap(widgetTypeFiles => this.exportJSZip(widgetTypeFiles, 'widget_types')),
catchError(e => {
this.handleExportError(e, 'widget-type.export-failed-error');
throw e;
})
);
}
public importWidgetType(): Observable<WidgetType> {
return this.openImportDialog('widget-type.import', 'widget-type.widget-type-file').pipe( return this.openImportDialog('widget-type.import', 'widget-type.widget-type-file').pipe(
mergeMap((widgetTypeDetails: WidgetTypeDetails) => { mergeMap((widgetTypeDetails: WidgetTypeDetails) => {
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) { if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) {
@ -283,7 +305,6 @@ export class ImportExportService {
type: 'error'})); type: 'error'}));
throw new Error('Invalid widget type file'); throw new Error('Invalid widget type file');
} else { } else {
widgetTypeDetails.bundleAlias = bundleAlias;
return this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails); return this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails);
} }
}), }),
@ -296,27 +317,22 @@ export class ImportExportService {
public exportWidgetsBundle(widgetsBundleId: string) { public exportWidgetsBundle(widgetsBundleId: string) {
this.widgetService.getWidgetsBundle(widgetsBundleId).subscribe( this.widgetService.getWidgetsBundle(widgetsBundleId).subscribe(
(widgetsBundle) => { (widgetsBundle) => {
const bundleAlias = widgetsBundle.alias; this.dialog.open<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogData,
const isSystem = widgetsBundle.tenantId.id === NULL_UUID; ExportWidgetsBundleDialogResult>(ExportWidgetsBundleDialogComponent, {
this.widgetService.getBundleWidgetTypesDetails(bundleAlias, isSystem).subscribe( disableClose: true,
(widgetTypesDetails) => { panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
widgetTypesDetails = widgetTypesDetails.sort((a, b) => a.createdTime - b.createdTime); data: {
const widgetsBundleItem: WidgetsBundleItem = { widgetsBundle
widgetsBundle: this.prepareExport(widgetsBundle), }
widgetTypes: [] }).afterClosed().subscribe(
}; (result) => {
for (const widgetTypeDetails of widgetTypesDetails) { if (result) {
if (isDefined(widgetTypeDetails.bundleAlias)) { if (result.exportWidgets) {
delete widgetTypeDetails.bundleAlias; this.exportWidgetsBundleWithWidgetTypes(widgetsBundle);
} else {
this.exportWidgetsBundleWithWidgetTypeFqns(widgetsBundle);
} }
widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails));
} }
let name = widgetsBundle.title;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(widgetsBundleItem, name);
},
(e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error');
} }
); );
}, },
@ -326,6 +342,43 @@ export class ImportExportService {
); );
} }
private exportWidgetsBundleWithWidgetTypes(widgetsBundle: WidgetsBundle) {
this.widgetService.getBundleWidgetTypesDetails(widgetsBundle.id.id).subscribe(
(widgetTypesDetails) => {
const widgetsBundleItem: WidgetsBundleItem = {
widgetsBundle: this.prepareExport(widgetsBundle),
widgetTypes: []
};
for (const widgetTypeDetails of widgetTypesDetails) {
widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails));
}
let name = widgetsBundle.title;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(widgetsBundleItem, name);
},
(e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error');
}
);
}
private exportWidgetsBundleWithWidgetTypeFqns(widgetsBundle: WidgetsBundle) {
this.widgetService.getBundleWidgetTypeFqns(widgetsBundle.id.id).subscribe(
(widgetTypeFqns) => {
const widgetsBundleItem: WidgetsBundleItem = {
widgetsBundle: this.prepareExport(widgetsBundle),
widgetTypeFqns
};
let name = widgetsBundle.title;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(widgetsBundleItem, name);
},
(e) => {
this.handleExportError(e, 'widgets-bundle.export-failed-error');
}
);
}
public importWidgetsBundle(): Observable<WidgetsBundle> { public importWidgetsBundle(): Observable<WidgetsBundle> {
return this.openImportDialog('widgets-bundle.import', 'widgets-bundle.widgets-bundle-file').pipe( return this.openImportDialog('widgets-bundle.import', 'widgets-bundle.widgets-bundle-file').pipe(
mergeMap((widgetsBundleItem: WidgetsBundleItem) => { mergeMap((widgetsBundleItem: WidgetsBundleItem) => {
@ -338,16 +391,32 @@ export class ImportExportService {
const widgetsBundle = widgetsBundleItem.widgetsBundle; const widgetsBundle = widgetsBundleItem.widgetsBundle;
return this.widgetService.saveWidgetsBundle(widgetsBundle).pipe( return this.widgetService.saveWidgetsBundle(widgetsBundle).pipe(
mergeMap((savedWidgetsBundle) => { mergeMap((savedWidgetsBundle) => {
const bundleAlias = savedWidgetsBundle.alias; if (widgetsBundleItem.widgetTypes?.length || widgetsBundleItem.widgetTypeFqns?.length) {
const widgetTypesDetails = widgetsBundleItem.widgetTypes; let widgetTypesObservable: Observable<Array<WidgetTypeDetails>>;
if (widgetTypesDetails.length) { if (widgetsBundleItem.widgetTypes?.length) {
const saveWidgetTypesObservables: Array<Observable<WidgetType>> = []; const widgetTypesDetails = widgetsBundleItem.widgetTypes;
for (const widgetTypeDetails of widgetTypesDetails) { const saveWidgetTypesObservables: Array<Observable<WidgetTypeDetails>> = [];
widgetTypeDetails.bundleAlias = bundleAlias; for (const widgetTypeDetails of widgetTypesDetails) {
saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails)); saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails));
}
widgetTypesObservable = forkJoin(saveWidgetTypesObservables);
} else {
widgetTypesObservable = of([]);
} }
return forkJoin(saveWidgetTypesObservables).pipe( return widgetTypesObservable.pipe(
map(() => savedWidgetsBundle) switchMap((widgetTypes) => {
let widgetTypeFqns = widgetTypes.map(w => w.fqn);
if (widgetsBundleItem.widgetTypeFqns?.length) {
widgetTypeFqns = widgetTypeFqns.concat(widgetsBundleItem.widgetTypeFqns);
}
if (widgetTypeFqns.length) {
return this.widgetService.updateWidgetsBundleWidgetFqns(savedWidgetsBundle.id.id, widgetTypeFqns).pipe(
map((res) => savedWidgetsBundle)
);
} else {
return of(savedWidgetsBundle);
}
})
); );
} else { } else {
return of(savedWidgetsBundle); return of(savedWidgetsBundle);
@ -636,19 +705,28 @@ export class ImportExportService {
this.downloadFile(content, filename, TEXT_TYPE); this.downloadFile(content, filename, TEXT_TYPE);
} }
public exportJSZip(data: object, filename: string) { public exportJSZip(data: object, filename: string): Observable<void> {
const exportJsSubjectSubject = new Subject<void>();
import('jszip').then((JSZip) => { import('jszip').then((JSZip) => {
const jsZip = new JSZip.default(); try {
for (const keyName in data) { const jsZip = new JSZip.default();
if (data.hasOwnProperty(keyName)) { for (const keyName in data) {
const valueData = data[keyName]; if (data.hasOwnProperty(keyName)) {
jsZip.file(keyName, valueData); const valueData = data[keyName];
jsZip.file(keyName, valueData);
}
} }
jsZip.generateAsync({type: 'blob'}).then(content => {
this.downloadFile(content, filename, ZIP_TYPE);
exportJsSubjectSubject.next(null);
}).catch(e => {
exportJsSubjectSubject.error(e);
});
} catch (e) {
exportJsSubjectSubject.error(e);
} }
jsZip.generateAsync({type: 'blob'}).then(content => {
this.downloadFile(content, filename, ZIP_TYPE);
});
}); });
return exportJsSubjectSubject.asObservable();
} }
private prepareRuleChain(ruleChain: RuleChain): RuleChain { private prepareRuleChain(ruleChain: RuleChain): RuleChain {
@ -765,16 +843,23 @@ export class ImportExportService {
if (isUndefined(widgetsBundleItem.widgetsBundle)) { if (isUndefined(widgetsBundleItem.widgetsBundle)) {
return false; return false;
} }
if (isUndefined(widgetsBundleItem.widgetTypes)) { if (isUndefined(widgetsBundleItem.widgetTypes) && isUndefined(widgetsBundleItem.widgetTypeFqns)) {
return false; return false;
} }
const widgetsBundle = widgetsBundleItem.widgetsBundle; const widgetsBundle = widgetsBundleItem.widgetsBundle;
if (isUndefined(widgetsBundle.title)) { if (isUndefined(widgetsBundle.title)) {
return false; return false;
} }
const widgetTypesDetails = widgetsBundleItem.widgetTypes; if (isDefined(widgetsBundleItem.widgetTypes)) {
for (const widgetTypeDetails of widgetTypesDetails) { const widgetTypesDetails = widgetsBundleItem.widgetTypes;
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) { for (const widgetTypeDetails of widgetTypesDetails) {
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) {
return false;
}
}
}
if (isDefined(widgetsBundleItem.widgetTypeFqns)) {
if (!Array.isArray(widgetsBundleItem.widgetTypeFqns)) {
return false; return false;
} }
} }

1
ui-ngx/src/app/modules/home/components/router-tabs.component.scss

@ -31,7 +31,6 @@
margin-left: 0; margin-left: 0;
} }
span { span {
overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }

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

@ -85,7 +85,6 @@ export class WidgetComponentService {
this.editingWidgetType = toWidgetType( this.editingWidgetType = toWidgetType(
{ {
widgetName: this.utils.editWidgetInfo.widgetName, widgetName: this.utils.editWidgetInfo.widgetName,
bundleAlias: 'customWidgetBundle',
fullFqn: 'system.customWidget', fullFqn: 'system.customWidget',
deprecated: false, deprecated: false,
type: this.utils.editWidgetInfo.type, type: this.utils.editWidgetInfo.type,
@ -104,7 +103,7 @@ export class WidgetComponentService {
hasBasicMode: this.utils.editWidgetInfo.hasBasicMode, hasBasicMode: this.utils.editWidgetInfo.hasBasicMode,
basicModeDirective: this.utils.editWidgetInfo.basicModeDirective, basicModeDirective: this.utils.editWidgetInfo.basicModeDirective,
defaultConfig: this.utils.editWidgetInfo.defaultConfig defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), undefined
); );
} }
const initSubject = new ReplaySubject<void>(); const initSubject = new ReplaySubject<void>();

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

@ -509,7 +509,6 @@ export interface IDynamicWidgetComponent {
export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor { export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescriptor {
widgetName: string; widgetName: string;
bundleAlias: string;
fullFqn: string; fullFqn: string;
deprecated: boolean; deprecated: boolean;
typeSettingsSchema?: string | any; typeSettingsSchema?: string | any;
@ -542,7 +541,6 @@ export interface WidgetConfigComponentData {
export const MissingWidgetType: WidgetInfo = { export const MissingWidgetType: WidgetInfo = {
type: widgetType.latest, type: widgetType.latest,
widgetName: 'Widget type not found', widgetName: 'Widget type not found',
bundleAlias: 'undefined',
fullFqn: 'undefined', fullFqn: 'undefined',
deprecated: false, deprecated: false,
sizeX: 8, sizeX: 8,
@ -568,7 +566,6 @@ export const MissingWidgetType: WidgetInfo = {
export const ErrorWidgetType: WidgetInfo = { export const ErrorWidgetType: WidgetInfo = {
type: widgetType.latest, type: widgetType.latest,
widgetName: 'Error loading widget', widgetName: 'Error loading widget',
bundleAlias: 'error',
fullFqn: 'error', fullFqn: 'error',
deprecated: false, deprecated: false,
sizeX: 8, sizeX: 8,
@ -611,7 +608,6 @@ export interface WidgetTypeInstance {
export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({ export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({
widgetName: widgetTypeEntity.name, widgetName: widgetTypeEntity.name,
bundleAlias: widgetTypeEntity.bundleAlias,
fullFqn: fullWidgetTypeFqn(widgetTypeEntity), fullFqn: fullWidgetTypeFqn(widgetTypeEntity),
deprecated: widgetTypeEntity.deprecated, deprecated: widgetTypeEntity.deprecated,
type: widgetTypeEntity.descriptor.type, type: widgetTypeEntity.descriptor.type,
@ -640,7 +636,7 @@ export const detailsToWidgetInfo = (widgetTypeDetailsEntity: WidgetTypeDetails):
}; };
export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
bundleAlias: string, createdTime: number): WidgetType => { createdTime: number): WidgetType => {
const descriptor: WidgetTypeDescriptor = { const descriptor: WidgetTypeDescriptor = {
type: widgetInfo.type, type: widgetInfo.type,
sizeX: widgetInfo.sizeX, sizeX: widgetInfo.sizeX,
@ -663,7 +659,6 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
id, id,
tenantId, tenantId,
createdTime, createdTime,
bundleAlias,
fqn: widgetTypeFqn(widgetInfo.fullFqn), fqn: widgetTypeFqn(widgetInfo.fullFqn),
name: widgetInfo.widgetName, name: widgetInfo.widgetName,
deprecated: widgetInfo.deprecated, deprecated: widgetInfo.deprecated,
@ -672,8 +667,8 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
}; };
export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
bundleAlias: string, createdTime: number): WidgetTypeDetails => { createdTime: number): WidgetTypeDetails => {
const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, bundleAlias, createdTime); const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime);
return { return {
...widgetTypeEntity, ...widgetTypeEntity,
description: widgetInfo.description, description: widgetInfo.description,

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

@ -36,7 +36,7 @@ import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-
import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component'; import { RepositoryAdminSettingsComponent } from '@home/pages/admin/repository-admin-settings.component';
import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component'; import { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-settings.component';
import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component'; import { TwoFactorAuthSettingsComponent } from '@home/pages/admin/two-factor-auth-settings.component';
import { widgetsBundlesRoutes } from '@home/pages/widget/widget-library-routing.module'; import { widgetsLibraryRoutes } from '@home/pages/widget/widget-library-routing.module';
import { RouterTabsComponent } from '@home/components/router-tabs.component'; import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module'; import { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module';
@ -67,10 +67,10 @@ const routes: Routes = [
children: [], children: [],
data: { data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
redirectTo: '/resources/widgets-bundles' redirectTo: '/resources/widgets-library'
} }
}, },
...widgetsBundlesRoutes, ...widgetsLibraryRoutes,
{ {
path: 'resources-library', path: 'resources-library',
data: { data: {

56
ui-ngx/src/app/modules/home/pages/widget/move-widget-type-dialog.component.html

@ -1,56 +0,0 @@
<!--
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.
-->
<form [formGroup]="moveWidgetTypeFormGroup" (ngSubmit)="move()">
<mat-toolbar color="primary">
<h2 translate>widget.move-widget-type</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset>
<span translate>widget.move-widget-type-text</span>
<tb-widgets-bundle-select fxFlex
formControlName="widgetsBundle"
required
[excludeBundleIds]="[data.currentBundleId]"
bundlesScope="{{bundlesScope}}">
</tb-widgets-bundle-select>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || moveWidgetTypeFormGroup.invalid
|| !moveWidgetTypeFormGroup.dirty">
{{ 'action.move' | translate }}
</button>
</div>
</form>

82
ui-ngx/src/app/modules/home/pages/widget/move-widget-type-dialog.component.ts

@ -1,82 +0,0 @@
///
/// 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, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
export interface MoveWidgetTypeDialogResult {
bundleId: string;
bundleAlias: string;
}
export interface MoveWidgetTypeDialogData {
currentBundleId: string;
}
@Component({
selector: 'tb-move-widget-type-dialog',
templateUrl: './move-widget-type-dialog.component.html',
styleUrls: []
})
export class MoveWidgetTypeDialogComponent extends
DialogComponent<MoveWidgetTypeDialogComponent, MoveWidgetTypeDialogResult> implements OnInit {
moveWidgetTypeFormGroup: UntypedFormGroup;
bundlesScope: string;
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MoveWidgetTypeDialogData,
public dialogRef: MatDialogRef<MoveWidgetTypeDialogComponent, MoveWidgetTypeDialogResult>,
public fb: UntypedFormBuilder) {
super(store, router, dialogRef);
const authUser = getCurrentAuthUser(store);
if (authUser.authority === Authority.TENANT_ADMIN) {
this.bundlesScope = 'tenant';
} else {
this.bundlesScope = 'system';
}
}
ngOnInit(): void {
this.moveWidgetTypeFormGroup = this.fb.group({
widgetsBundle: [null, [Validators.required]]
});
}
cancel(): void {
this.dialogRef.close(null);
}
move(): void {
const widgetsBundle: WidgetsBundle = this.moveWidgetTypeFormGroup.get('widgetsBundle').value;
const result: MoveWidgetTypeDialogResult = {
bundleId: widgetsBundle.id.id,
bundleAlias: widgetsBundle.alias
};
this.dialogRef.close(result);
}
}

5
ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.html

@ -38,11 +38,6 @@
{{ 'widget.title-required' | translate }} {{ 'widget.title-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<tb-widgets-bundle-select fxFlex
formControlName="widgetsBundle"
required
bundlesScope="{{bundlesScope}}">
</tb-widgets-bundle-select>
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions fxLayoutAlign="end center"> <div mat-dialog-actions fxLayoutAlign="end center">

22
ui-ngx/src/app/modules/home/pages/widget/save-widget-type-as-dialog.component.ts

@ -21,14 +21,9 @@ import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
export interface SaveWidgetTypeAsDialogResult { export interface SaveWidgetTypeAsDialogResult {
widgetName: string; widgetName: string;
bundleId: string;
bundleAlias: string;
} }
@Component({ @Component({
@ -41,26 +36,16 @@ export class SaveWidgetTypeAsDialogComponent extends
saveWidgetTypeAsFormGroup: UntypedFormGroup; saveWidgetTypeAsFormGroup: UntypedFormGroup;
bundlesScope: string;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>, public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>,
public fb: UntypedFormBuilder) { public fb: UntypedFormBuilder) {
super(store, router, dialogRef); super(store, router, dialogRef);
const authUser = getCurrentAuthUser(store);
if (authUser.authority === Authority.TENANT_ADMIN) {
this.bundlesScope = 'tenant';
} else {
this.bundlesScope = 'system';
}
} }
ngOnInit(): void { ngOnInit(): void {
this.saveWidgetTypeAsFormGroup = this.fb.group({ this.saveWidgetTypeAsFormGroup = this.fb.group({
title: [null, [Validators.required]], title: [null, [Validators.required]]
widgetsBundle: [null, [Validators.required]]
}); });
} }
@ -70,11 +55,8 @@ export class SaveWidgetTypeAsDialogComponent extends
saveAs(): void { saveAs(): void {
const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value; const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value;
const widgetsBundle: WidgetsBundle = this.saveWidgetTypeAsFormGroup.get('widgetsBundle').value;
const result: SaveWidgetTypeAsDialogResult = { const result: SaveWidgetTypeAsDialogResult = {
widgetName, widgetName
bundleId: widgetsBundle.id.id,
bundleAlias: widgetsBundle.alias
}; };
this.dialogRef.close(result); this.dialogRef.close(result);
} }

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

@ -22,7 +22,7 @@
<mat-form-field class="tb-widget-title tb-appearance-transparent"> <mat-form-field class="tb-widget-title tb-appearance-transparent">
<input [disabled]="isReadOnly" matInput required maxlength="255" <input [disabled]="isReadOnly" matInput required maxlength="255"
[(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true" [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true"
placeholder="{{ 'widget.title' | translate }}"/> placeholder="{{ 'widget.widget-title' | translate }}"/>
</mat-form-field> </mat-form-field>
<mat-form-field class="tb-appearance-transparent"> <mat-form-field class="tb-appearance-transparent">
<mat-select [disabled]="isReadOnly" matInput placeholder="{{ 'widget.type' | translate }}" <mat-select [disabled]="isReadOnly" matInput placeholder="{{ 'widget.type' | translate }}"
@ -66,15 +66,6 @@
<mat-icon>save_as</mat-icon> <mat-icon>save_as</mat-icon>
<span translate>action.saveAs</span> <span translate>action.saveAs</span>
</button> </button>
<button mat-raised-button
fxHide.lt-md [disabled]="(isLoading$ | async) || moveDisabled()"
(click)="moveWidget()"
[tb-circular-progress]="moveWidgetPending"
matTooltip="{{ 'widget.move' | translate }} (Shift + CTRL + M)"
matTooltipPosition="below">
<tb-icon matButtonIcon>mdi:content-save-move</tb-icon>
<span translate>action.move</span>
</button>
<button mat-button <button mat-button
fxHide.lt-lg fxHide.lt-lg
(click)="fullscreen = !fullscreen" (click)="fullscreen = !fullscreen"
@ -118,12 +109,6 @@
<mat-icon>save_as</mat-icon> <mat-icon>save_as</mat-icon>
<span translate>action.saveAs</span> <span translate>action.saveAs</span>
</button> </button>
<button mat-menu-item
[disabled]="(isLoading$ | async) || moveDisabled()"
(click)="moveWidget()">
<tb-icon matMenuItemIcon>mdi:content-save-move</tb-icon>
<span translate>action.move</span>
</button>
</mat-menu> </mat-menu>
</mat-toolbar> </mat-toolbar>
<div fxFlex style="position: relative;"> <div fxFlex style="position: relative;">

68
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts

@ -25,7 +25,6 @@ import {
ViewChild, ViewChild,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
@ -59,11 +58,6 @@ import { widgetEditorCompleter } from '@home/pages/widget/widget-editor.models';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models'; import { beautifyCss, beautifyHtml, beautifyJs } from '@shared/models/beautify.models';
import {
MoveWidgetTypeDialogComponent,
MoveWidgetTypeDialogData,
MoveWidgetTypeDialogResult
} from '@home/pages/widget/move-widget-type-dialog.component';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
// @dynamic // @dynamic
@ -124,7 +118,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
isReadOnly: boolean; isReadOnly: boolean;
widgetsBundle: WidgetsBundle;
widgetTypeDetails: WidgetTypeDetails; widgetTypeDetails: WidgetTypeDetails;
widget: WidgetInfo; widget: WidgetInfo;
origWidget: WidgetInfo; origWidget: WidgetInfo;
@ -155,7 +148,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
iframeWidgetEditModeInited = false; iframeWidgetEditModeInited = false;
saveWidgetPending = false; saveWidgetPending = false;
saveWidgetAsPending = false; saveWidgetAsPending = false;
moveWidgetPending = false;
gotError = false; gotError = false;
errorMarkers: number[] = []; errorMarkers: number[] = [];
@ -191,14 +183,13 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
} }
private init(data: any) { private init(data: any) {
this.widgetsBundle = data.widgetsBundle; this.widgetTypeDetails = data.widgetEditorData.widgetTypeDetails;
this.widget = data.widgetEditorData.widget;
if (this.authUser.authority === Authority.TENANT_ADMIN) { if (this.authUser.authority === Authority.TENANT_ADMIN) {
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID; this.isReadOnly = this.widgetTypeDetails && this.widgetTypeDetails.tenantId.id === NULL_UUID;
} else { } else {
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN; this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
} }
this.widgetTypeDetails = data.widgetEditorData.widgetTypeDetails;
this.widget = data.widgetEditorData.widget;
if (this.widgetTypeDetails) { if (this.widgetTypeDetails) {
const config = JSON.parse(this.widget.defaultConfig); const config = JSON.parse(this.widget.defaultConfig);
this.widget.defaultConfig = JSON.stringify(config); this.widget.defaultConfig = JSON.stringify(config);
@ -259,16 +250,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}, ['INPUT', 'SELECT', 'TEXTAREA'], }, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.saveAs')) this.translate.instant('widget.saveAs'))
); );
this.hotKeys.push(
new Hotkey('shift+ctrl+m', (event: KeyboardEvent) => {
if (!getCurrentIsLoading(this.store) && !this.moveDisabled()) {
event.preventDefault();
this.moveWidget();
}
return false;
}, ['INPUT', 'SELECT', 'TEXTAREA'],
this.translate.instant('widget.move'))
);
this.hotKeys.push( this.hotKeys.push(
new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => { new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
@ -562,7 +543,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
private commitSaveWidget() { private commitSaveWidget() {
const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : undefined; const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : undefined;
const createdTime = (this.widgetTypeDetails && this.widgetTypeDetails.createdTime) ? this.widgetTypeDetails.createdTime : undefined; const createdTime = (this.widgetTypeDetails && this.widgetTypeDetails.createdTime) ? this.widgetTypeDetails.createdTime : undefined;
this.widgetService.saveWidgetTypeDetails(this.widget, id, this.widgetsBundle.alias, createdTime).subscribe({ this.widgetService.saveWidgetTypeDetails(this.widget, id, createdTime).subscribe({
next: (widgetTypeDetails) => { next: (widgetTypeDetails) => {
this.saveWidgetPending = false; this.saveWidgetPending = false;
if (!this.widgetTypeDetails?.id) { if (!this.widgetTypeDetails?.id) {
@ -594,11 +575,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
config.title = this.widget.widgetName; config.title = this.widget.widgetName;
this.widget.defaultConfig = JSON.stringify(config); this.widget.defaultConfig = JSON.stringify(config);
this.isDirty = false; this.isDirty = false;
this.widgetService.saveWidgetTypeDetails(this.widget, undefined, saveWidgetAsData.bundleAlias, undefined).subscribe( this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined).subscribe(
{ {
next: (widgetTypeDetails) => { next: (widgetTypeDetails) => {
this.saveWidgetAsPending = false; this.saveWidgetAsPending = false;
this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeDetails.id.id}`); this.router.navigate(['..', widgetTypeDetails.id.id], {relativeTo: this.route});
}, },
error: () => { error: () => {
this.saveWidgetAsPending = false; this.saveWidgetAsPending = false;
@ -655,34 +636,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
this.applyWidgetScript(); this.applyWidgetScript();
} }
moveWidget() {
this.moveWidgetPending = true;
this.dialog.open<MoveWidgetTypeDialogComponent, MoveWidgetTypeDialogData,
MoveWidgetTypeDialogResult>(MoveWidgetTypeDialogComponent, {
disableClose: true,
data: {
currentBundleId: this.widgetsBundle.id.id
},
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(moveWidgetTypeData) => {
if (moveWidgetTypeData) {
this.widgetService.moveWidgetType(this.widgetTypeDetails.id.id, moveWidgetTypeData.bundleAlias).subscribe({
next: (widgetTypeDetails) => {
this.moveWidgetPending = false;
this.router.navigateByUrl(`/widgets-bundles/${moveWidgetTypeData.bundleId}/widgetTypes/${widgetTypeDetails.id.id}`);
},
error: () => {
this.moveWidgetPending = false;
}
});
} else {
this.moveWidgetPending = false;
}
}
);
}
undoDisabled(): boolean { undoDisabled(): boolean {
return !this.isDirty return !this.isDirty
|| !this.iframeWidgetEditModeInited || !this.iframeWidgetEditModeInited
@ -704,15 +657,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
|| this.saveWidgetAsPending; || this.saveWidgetAsPending;
} }
moveDisabled(): boolean {
return this.isReadOnly
|| !this.widgetTypeDetails?.id
|| !this.iframeWidgetEditModeInited
|| this.saveWidgetPending
|| this.saveWidgetAsPending
|| this.moveWidgetPending;
}
beautifyCss(): void { beautifyCss(): void {
beautifyCss(this.widget.templateCss, {indent_size: 4}).subscribe( beautifyCss(this.widget.templateCss, {indent_size: 4}).subscribe(
(res) => { (res) => {

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

@ -20,7 +20,6 @@ import { ActivatedRouteSnapshot, Resolve, RouterModule, Routes } from '@angular/
import { EntitiesTableComponent } from '../../components/entity/entities-table.component'; import { EntitiesTableComponent } from '../../components/entity/entities-table.component';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { WidgetsBundlesTableConfigResolver } from '@modules/home/pages/widget/widgets-bundles-table-config.resolver'; import { WidgetsBundlesTableConfigResolver } from '@modules/home/pages/widget/widgets-bundles-table-config.resolver';
import { WidgetLibraryComponent } from '@home/pages/widget/widget-library.component';
import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb'; import { BreadCrumbConfig, BreadCrumbLabelFunction } from '@shared/components/breadcrumb';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
@ -28,10 +27,11 @@ import { WidgetService } from '@core/http/widget.service';
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { detailsToWidgetInfo, WidgetInfo } from '@home/models/widget-component.models'; import { detailsToWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { widgetType, WidgetTypeDetails } from '@app/shared/models/widget.models'; import { widgetType, WidgetTypeDetails, WidgetTypeInfo } from '@app/shared/models/widget.models';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { WidgetsData } from '@home/models/dashboard-component.models'; import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { WidgetTypesTableConfigResolver } from '@home/pages/widget/widget-types-table-config.resolver';
import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component';
export interface WidgetEditorData { export interface WidgetEditorData {
widgetTypeDetails: WidgetTypeDetails; widgetTypeDetails: WidgetTypeDetails;
@ -45,14 +45,24 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
} }
resolve(route: ActivatedRouteSnapshot): Observable<WidgetsBundle> { resolve(route: ActivatedRouteSnapshot): Observable<WidgetsBundle> {
let widgetsBundleId = route.params.widgetsBundleId; const widgetsBundleId = route.params.widgetsBundleId;
if (!widgetsBundleId) {
widgetsBundleId = route.parent.params.widgetsBundleId;
}
return this.widgetsService.getWidgetsBundle(widgetsBundleId); return this.widgetsService.getWidgetsBundle(widgetsBundleId);
} }
} }
@Injectable()
export class WidgetsBundleWidgetsResolver implements Resolve<Array<WidgetTypeInfo>> {
constructor(private widgetsService: WidgetService) {
}
resolve(route: ActivatedRouteSnapshot): Observable<Array<WidgetTypeInfo>> {
const widgetsBundleId = route.params.widgetsBundleId;
return this.widgetsService.getBundleWidgetTypeInfos(widgetsBundleId);
}
}
/*
@Injectable() @Injectable()
export class WidgetsTypesDataResolver implements Resolve<WidgetsData> { export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
@ -60,15 +70,12 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
} }
resolve(route: ActivatedRouteSnapshot): Observable<WidgetsData> { resolve(route: ActivatedRouteSnapshot): Observable<WidgetsData> {
const widgetsBundle: WidgetsBundle = route.parent.data.widgetsBundle; const widgetsBundleId = route.params.widgetsBundleId;
const bundleAlias = widgetsBundle.alias; return this.widgetsService.loadBundleLibraryWidgets(widgetsBundleId).pipe(
const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
return this.widgetsService.loadBundleLibraryWidgets(bundleAlias,
isSystem).pipe(
map((widgets) => ({ widgets }) map((widgets) => ({ widgets })
)); ));
} }
} }*/
@Injectable() @Injectable()
export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> { export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> {
@ -103,7 +110,7 @@ export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> {
} }
} }
export const widgetTypesBreadcumbLabelFunction: BreadCrumbLabelFunction<any> = ((route, translate) => export const widgetsBundleWidgetsBreadcumbLabelFunction: BreadCrumbLabelFunction<any> = ((route, translate) =>
route.data.widgetsBundle.title); route.data.widgetsBundle.title);
export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetEditorComponent> = export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetEditorComponent> =
@ -111,7 +118,49 @@ export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetE
component?.widget?.widgetName ? component?.widget?.widgetName ?
(component.widget.widgetName + (component.widget.deprecated ? ` (${translate.instant('widget.deprecated')})` : '')) : ''); (component.widget.widgetName + (component.widget.deprecated ? ` (${translate.instant('widget.deprecated')})` : '')) : '');
export const widgetsBundlesRoutes: Routes = [ const widgetTypesRoutes: Routes = [
{
path: 'widget-types',
data: {
breadcrumb: {
label: 'widget.widgets',
icon: 'now_widgets'
}
},
children: [
{
path: '',
component: EntitiesTableComponent,
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.widgets'
},
resolve: {
entitiesTableConfig: WidgetTypesTableConfigResolver
}
},
{
path: ':widgetTypeId',
component: WidgetEditorComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.editor',
breadcrumb: {
labelFunction: widgetEditorBreadcumbLabelFunction,
icon: 'insert_chart'
} as BreadCrumbConfig<WidgetEditorComponent>,
hideTabs: true
},
resolve: {
widgetEditorData: WidgetEditorDataResolver
}
}
]
},
];
const widgetsBundlesRoutes: Routes = [
{ {
path: 'widgets-bundles', path: 'widgets-bundles',
data: { data: {
@ -133,73 +182,91 @@ export const widgetsBundlesRoutes: Routes = [
} }
}, },
{ {
path: ':widgetsBundleId/widgetTypes', path: ':widgetsBundleId',
component: WidgetsBundleWidgetsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: { data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widgets-bundle.widgets-bundle-widgets',
breadcrumb: { breadcrumb: {
labelFunction: widgetTypesBreadcumbLabelFunction, labelFunction: widgetsBundleWidgetsBreadcumbLabelFunction,
icon: 'now_widgets' icon: 'now_widgets'
} as BreadCrumbConfig<any> } as BreadCrumbConfig<any>,
hideTabs: true
}, },
resolve: { resolve: {
widgetsBundle: WidgetsBundleResolver widgetsBundle: WidgetsBundleResolver,
}, widgets: WidgetsBundleWidgetsResolver
children: [ }
{
path: '',
component: WidgetLibraryComponent,
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.widget-library'
},
resolve: {
widgetsData: WidgetsTypesDataResolver
}
},
{
path: ':widgetTypeId',
component: WidgetEditorComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widget.editor',
breadcrumb: {
labelFunction: widgetEditorBreadcumbLabelFunction,
icon: 'insert_chart'
} as BreadCrumbConfig<WidgetEditorComponent>
},
resolve: {
widgetEditorData: WidgetEditorDataResolver
}
}
]
} }
] ]
}, },
]; ];
export const widgetsLibraryRoutes: Routes = [
{
path: 'widgets-library',
component: RouterTabsComponent,
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: {
label: 'widget.widget-library',
icon: 'now_widgets'
}
},
children: [
{
path: '',
children: [],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
redirectTo: '/resources/widgets-library/widget-types'
}
},
...widgetTypesRoutes,
...widgetsBundlesRoutes
]
}
];
const routes: Routes = [ const routes: Routes = [
{ {
path: 'widgets-bundles', path: 'widgets-bundles',
pathMatch: 'full', pathMatch: 'full',
redirectTo: '/resources/widgets-bundles' redirectTo: '/resources/widgets-library/widgets-bundles'
},
{
path: 'resources/widgets-bundles',
pathMatch: 'full',
redirectTo: '/resources/widgets-library/widgets-bundles'
}, },
{ {
path: 'widgets-bundles/:widgetsBundleId/widgetTypes', path: 'widgets-bundles/:widgetsBundleId/widgetTypes',
pathMatch: 'full', pathMatch: 'full',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes', redirectTo: '/resources/widgets-library/widgets-bundles/:widgetsBundleId'
},
{
path: 'resources/widgets-bundles/:widgetsBundleId/widgetTypes',
pathMatch: 'full',
redirectTo: '/resources/widgets-library/widgets-bundles/:widgetsBundleId'
}, },
{ {
path: 'widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId', path: 'widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId',
pathMatch: 'full', pathMatch: 'full',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId', redirectTo: '/resources/widgets-library/widget-types/:widgetTypeId'
},
{
path: 'resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetTypeId',
pathMatch: 'full',
redirectTo: '/resources/widgets-library/widget-types/:widgetTypeId'
}, },
{ {
path: 'widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType', path: 'widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetType', redirectTo: '/resources/widgets-library/widget-types/:widgetType',
}, },
{ {
path: 'resources/widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType', path: 'resources/widgets-bundles/:widgetsBundleId/widgetTypes/add/:widgetType',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetType', redirectTo: '/resources/widgets-library/widget-types/:widgetType',
} }
]; ];
@ -208,9 +275,11 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
providers: [ providers: [
WidgetTypesTableConfigResolver,
WidgetsBundlesTableConfigResolver, WidgetsBundlesTableConfigResolver,
WidgetsBundleResolver, WidgetsBundleResolver,
WidgetsTypesDataResolver, WidgetsBundleWidgetsResolver,
// WidgetsTypesDataResolver,
WidgetEditorDataResolver WidgetEditorDataResolver
] ]
}) })

42
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.html

@ -1,42 +0,0 @@
<!--
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.
-->
<section [fxShow]="!(isLoading$ | async) && widgetsData.widgets.length === 0" fxLayoutAlign="center center"
style="display: flex; z-index: 1;"
class="tb-absolute-fill">
<button mat-button *ngIf="!isReadOnly" class="tb-add-new-widget" (click)="addWidgetType($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
{{ 'widget.add-widget-type' | translate }}
</button>
<span translate *ngIf="isReadOnly"
fxLayoutAlign="center center"
style="display: flex;"
class="mat-headline-5 tb-absolute-fill">widgets-bundle.empty</span>
</section>
<tb-dashboard class="tb-widget-library"
#dashboard
[aliasController]="aliasController"
[widgets]="widgetsData.widgets"
[widgetLayouts]="widgetsData.widgetLayouts"
[isEdit]="false"
[isEditActionEnabled]="true"
[isExportActionEnabled]="true"
[isRemoveActionEnabled]="!isReadOnly"
[disableWidgetInteraction]="true"
[callbacks]="dashboardCallbacks"></tb-dashboard>
<tb-footer-fab-buttons [fxShow]="!isReadOnly" [footerFabButtons]="footerFabButtons">
</tb-footer-fab-buttons>

205
ui-ngx/src/app/modules/home/pages/widget/widget-library.component.ts

@ -1,205 +0,0 @@
///
/// 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, OnInit, ViewChild } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { ActivatedRoute, Router } from '@angular/router';
import { Authority } from '@shared/models/authority.enum';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { of } from 'rxjs';
import { Widget, widgetType } from '@app/shared/models/widget.models';
import { WidgetService } from '@core/http/widget.service';
import { map, mergeMap } from 'rxjs/operators';
import { DialogService } from '@core/services/dialog.service';
import { FooterFabButtons } from '@app/shared/components/footer-fab-buttons.component';
import { DashboardCallbacks, IDashboardComponent, WidgetsData } from '@home/models/dashboard-component.models';
import { IAliasController, IStateController, StateParams } from '@app/core/api/widget-api.models';
import { AliasController } from '@core/api/alias-controller';
import { MatDialog } from '@angular/material/dialog';
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
import { TranslateService } from '@ngx-translate/core';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { UtilsService } from '@core/services/utils.service';
import { EntityService } from '@core/http/entity.service';
@Component({
selector: 'tb-widget-library',
templateUrl: './widget-library.component.html',
styleUrls: ['./widget-library.component.scss']
})
export class WidgetLibraryComponent extends PageComponent implements OnInit {
authUser: AuthUser;
isReadOnly: boolean;
widgetsBundle: WidgetsBundle;
widgetsData: WidgetsData;
footerFabButtons: FooterFabButtons = {
fabTogglerName: 'widget.add-widget-type',
fabTogglerIcon: 'add',
buttons: [
{
name: 'widget-type.create-new-widget-type',
icon: 'insert_drive_file',
onAction: ($event) => {
this.addWidgetType($event);
}
},
{
name: 'widget-type.import',
icon: 'file_upload',
onAction: ($event) => {
this.importWidgetType($event);
}
}
]
};
dashboardCallbacks: DashboardCallbacks = {
onEditWidget: this.openWidgetType.bind(this),
onWidgetClicked: this.openWidgetType.bind(this),
onExportWidget: this.exportWidgetType.bind(this),
onRemoveWidget: this.removeWidgetType.bind(this)
};
aliasController: IAliasController = new AliasController(this.utils,
this.entityService,
this.translate,
() => ({
getStateParams: (): StateParams => ({})
} as IStateController),
{},
{});
@ViewChild('dashboard', {static: true}) dashboard: IDashboardComponent;
constructor(protected store: Store<AppState>,
private route: ActivatedRoute,
private router: Router,
private widgetService: WidgetService,
private dialogService: DialogService,
private importExport: ImportExportService,
private dialog: MatDialog,
private translate: TranslateService,
private utils: UtilsService,
private entityService: EntityService) {
super(store);
this.authUser = getCurrentAuthUser(this.store);
this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
this.widgetsData = this.route.snapshot.data.widgetsData;
if (this.authUser.authority === Authority.TENANT_ADMIN) {
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
} else {
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
}
}
ngOnInit(): void {
}
addWidgetType($event: Event): void {
this.openWidgetType($event);
}
importWidgetType($event: Event): void {
if ($event) {
$event.stopPropagation();
}
this.importExport.importWidgetType(this.widgetsBundle.alias).subscribe(
(widgetTypeInstance) => {
if (widgetTypeInstance) {
this.reload();
}
}
);
}
private reload() {
const bundleAlias = this.widgetsBundle.alias;
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
this.widgetService.loadBundleLibraryWidgets(bundleAlias, isSystem).subscribe(
(widgets) => {
this.widgetsData = {widgets};
}
);
}
openWidgetType($event: Event, widget?: Widget): void {
if ($event) {
$event.stopPropagation();
}
if (widget) {
this.router.navigate([widget.typeId.id], {relativeTo: this.route});
} else {
this.dialog.open<SelectWidgetTypeDialogComponent, any,
widgetType>(SelectWidgetTypeDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(type) => {
if (type) {
this.router.navigate([type], {relativeTo: this.route});
}
}
);
}
}
exportWidgetType($event: Event, widget: Widget): void {
if ($event) {
$event.stopPropagation();
}
this.importExport.exportWidgetType(widget.typeId.id);
}
removeWidgetType($event: Event, widget: Widget): void {
if ($event) {
$event.stopPropagation();
}
this.dialogService.confirm(
this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}),
this.translate.instant('widget.remove-widget-type-text'),
this.translate.instant('action.no'),
this.translate.instant('action.yes'),
).pipe(
mergeMap((result) => {
if (result) {
return this.widgetService.deleteWidgetType(widget.typeFullFqn);
} else {
return of(false);
}
}),
map((result) => {
if (result !== false) {
this.reload();
return true;
} else {
return false;
}
}
)).subscribe();
}
}

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

@ -20,21 +20,26 @@ import { SharedModule } from '@shared/shared.module';
import { WidgetsBundleComponent } from '@modules/home/pages/widget/widgets-bundle.component'; import { WidgetsBundleComponent } from '@modules/home/pages/widget/widgets-bundle.component';
import { WidgetLibraryRoutingModule } from '@modules/home/pages/widget/widget-library-routing.module'; import { WidgetLibraryRoutingModule } from '@modules/home/pages/widget/widget-library-routing.module';
import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { HomeComponentsModule } from '@modules/home/components/home-components.module';
import { WidgetLibraryComponent } from './widget-library.component';
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component'; import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component'; import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.component';
import { WidgetsBundleTabsComponent } from '@home/pages/widget/widgets-bundle-tabs.component'; import { WidgetsBundleTabsComponent } from '@home/pages/widget/widgets-bundle-tabs.component';
import { MoveWidgetTypeDialogComponent } from '@home/pages/widget/move-widget-type-dialog.component'; import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component';
import { WidgetTypeTabsComponent } from '@home/pages/widget/widget-type-tabs.component';
import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component';
import { WidgetTypeAutocompleteComponent } from '@home/pages/widget/widget-type-autocomplete.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
WidgetTypeComponent,
WidgetsBundleComponent, WidgetsBundleComponent,
WidgetLibraryComponent, // WidgetLibraryComponent,
WidgetTypeAutocompleteComponent,
WidgetsBundleWidgetsComponent,
WidgetEditorComponent, WidgetEditorComponent,
SelectWidgetTypeDialogComponent, SelectWidgetTypeDialogComponent,
SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogComponent,
MoveWidgetTypeDialogComponent, WidgetTypeTabsComponent,
WidgetsBundleTabsComponent WidgetsBundleTabsComponent
], ],
imports: [ imports: [

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

@ -0,0 +1,59 @@
<!--
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]="selectWidgetTypeFormGroup" class="mat-block" [floatLabel]="floatLabel"
[appearance]="appearance" [subscriptSizing]="subscriptSizing">
<mat-label *ngIf="label">{{ label }}</mat-label>
<input matInput type="text"
#widgetTypeInput
placeholder="{{ placeholder }}"
formControlName="widgetType"
(focusin)="onFocus()"
[required]="required"
[matAutocomplete]="widgetTypeAutocomplete">
<button *ngIf="selectWidgetTypeFormGroup.get('widgetType').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 tb-widget-type-autocomplete"
#widgetTypeAutocomplete
[displayWith]="displayWidgetTypeFn">
<mat-option *ngFor="let widgetType of filteredWidgetTypes | async" [value]="widgetType">
<div class="tb-widget-type-option-container">
<img class="tb-widget-type-option-image-preview" [src]="getPreviewImage(widgetType.image)" alt="{{ widgetType.name }}">
<div class="tb-widget-type-option-details">
<div class="tb-widget-type-option-text" [innerHTML]="widgetType.name | highlight:searchText:true"></div>
<div *ngIf="widgetType.deprecated" class="tb-widget-type-option-deprecated">{{ 'widget.deprecated' | translate }}</div>
</div>
</div>
</mat-option>
<mat-option *ngIf="!(filteredWidgetTypes | async)?.length" [value]="null">
<span>
{{ translate.get('widget.no-widgets-matching', {entity: searchText}) | async }}
</span>
</mat-option>
</mat-autocomplete>
<mat-error>
<ng-content select="[tb-error]"></ng-content>
</mat-error>
<mat-hint>
<ng-content select="[tb-hint]"></ng-content>
</mat-hint>
</mat-form-field>

49
ui-ngx/src/app/modules/home/pages/widget/widget-type-autocomplete.component.scss

@ -0,0 +1,49 @@
/**
* 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-widget-type-autocomplete {
.mat-mdc-option {
.mdc-list-item__primary-text {
width: 100%;
.tb-widget-type-option-container {
display: flex;
gap: 8px;
align-items: center;
}
.tb-widget-type-option-image-preview {
width: 36px;
max-height: 100%;
object-fit: contain;
border-radius: 6px;
}
.tb-widget-type-option-details {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
.tb-widget-type-option-text {
font-size: 14px;
font-weight: 400;
letter-spacing: 0.2px;
}
.tb-widget-type-option-deprecated {
font-size: 13px;
font-weight: 400;
color: rgba(0, 0, 0, 0.54);
}
}
}
}
}

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

@ -0,0 +1,227 @@
///
/// 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,
ElementRef,
forwardRef,
Input,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { emptyPageData } from '@shared/models/page/page-data';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TranslateService } from '@ngx-translate/core';
import { FloatLabelType, MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
import { WidgetTypeInfo } from '@shared/models/widget.models';
import { coerceBoolean } from '@shared/decorators/coercion';
import { WidgetService } from '@core/http/widget.service';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-widget-type-autocomplete',
templateUrl: './widget-type-autocomplete.component.html',
styleUrls: ['./widget-type-autocomplete.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetTypeAutocompleteComponent),
multi: true
}],
encapsulation: ViewEncapsulation.None
})
export class WidgetTypeAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit {
private dirty = false;
selectWidgetTypeFormGroup: UntypedFormGroup;
modelValue: WidgetTypeInfo | null;
@Input()
label = this.translate.instant('widget.widget');
@Input()
placeholder: string;
@Input()
floatLabel: FloatLabelType = 'auto';
@Input()
appearance: MatFormFieldAppearance = 'fill';
@Input()
subscriptSizing: SubscriptSizing = 'fixed';
@Input()
@coerceBoolean()
required: boolean;
@Input()
disabled: boolean;
@Input()
excludeWidgetTypeIds: Array<string>;
@ViewChild('widgetTypeInput', {static: true}) widgetTypeInput: ElementRef;
filteredWidgetTypes: Observable<Array<WidgetTypeInfo>>;
searchText = '';
private propagateChange = (v: any) => { };
constructor(private store: Store<AppState>,
public translate: TranslateService,
private widgetService: WidgetService,
private sanitizer: DomSanitizer,
private fb: UntypedFormBuilder) {
this.selectWidgetTypeFormGroup = this.fb.group({
widgetType: [null]
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
ngOnInit() {
this.filteredWidgetTypes = this.selectWidgetTypeFormGroup.get('widgetType').valueChanges
.pipe(
debounceTime(150),
tap(value => {
let modelValue;
if (typeof value === 'string' || !value) {
modelValue = null;
} else {
modelValue = value;
}
this.updateView(modelValue);
}),
map(value => value ? (typeof value === 'string' ? value : value.name) : ''),
distinctUntilChanged(),
switchMap(name => this.fetchWidgetTypes(name) ),
share()
);
}
ngAfterViewInit(): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.selectWidgetTypeFormGroup.disable({emitEvent: false});
} else {
this.selectWidgetTypeFormGroup.enable({emitEvent: false});
}
}
writeValue(value: WidgetTypeInfo | string | null): void {
this.searchText = '';
if (value != null) {
if (typeof value === 'string') {
this.widgetService.getWidgetTypeInfoById(value).subscribe(
(widgetType) => {
this.modelValue = widgetType;
this.selectWidgetTypeFormGroup.get('widgetType').patchValue(widgetType, {emitEvent: false});
}
);
} else {
this.modelValue = value;
this.selectWidgetTypeFormGroup.get('widgetType').patchValue(value, {emitEvent: false});
}
} else {
this.modelValue = null;
this.selectWidgetTypeFormGroup.get('widgetType').patchValue('', {emitEvent: false});
}
this.dirty = true;
}
updateView(value: WidgetTypeInfo | null) {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
displayWidgetTypeFn(widgetType?: WidgetTypeInfo): string | undefined {
return widgetType ? widgetType.name : undefined;
}
getPreviewImage(imageUrl: string | null): SafeUrl | string {
if (isDefinedAndNotNull(imageUrl)) {
return this.sanitizer.bypassSecurityTrustUrl(imageUrl);
}
return '/assets/widget-preview-empty.svg';
}
fetchWidgetTypes(searchText?: string): Observable<Array<WidgetTypeInfo>> {
this.searchText = searchText;
const pageLink = new PageLink(10, 0, searchText, {
property: 'name',
direction: Direction.ASC
});
return this.getWidgetTypes(pageLink);
}
getWidgetTypes(pageLink: PageLink,
result: Array<WidgetTypeInfo> = []): Observable<Array<WidgetTypeInfo>> {
return this.widgetService.getWidgetTypes(pageLink, true).pipe(
catchError(() => of(emptyPageData<WidgetTypeInfo>())),
switchMap((data) => {
if (this.excludeWidgetTypeIds?.length) {
const filtered = data.data.filter(w => !this.excludeWidgetTypeIds.includes(w.id.id));
result = result.concat(filtered);
} else {
result = data.data;
}
if (result.length >= pageLink.pageSize || !this.excludeWidgetTypeIds?.length || !data.hasNext) {
return of(result);
} else {
return this.getWidgetTypes(pageLink.nextPageLink(), result);
}
})
);
}
onFocus() {
if (this.dirty) {
this.selectWidgetTypeFormGroup.get('widgetType').updateValueAndValidity({onlySelf: true});
this.dirty = false;
}
}
clear() {
this.selectWidgetTypeFormGroup.get('widgetType').patchValue('');
setTimeout(() => {
this.widgetTypeInput.nativeElement.blur();
this.widgetTypeInput.nativeElement.focus();
}, 0);
}
}

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

@ -0,0 +1,23 @@
<!--
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-tab *ngIf="isTenantWidgetType() && authUser.authority === authorities.TENANT_ADMIN"
label="{{ 'version-control.version-control' | translate }}" #versionControlTab="matTab">
<tb-version-control detailsMode="true" singleEntityMode="true"
(versionRestored)="entitiesTableConfig.updateData()"
[active]="versionControlTab.isActive" [entityId]="entity.id" [entityName]="entity.name" [externalEntityId]="entity.externalId || entity.id"></tb-version-control>
</mat-tab>

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

@ -0,0 +1,43 @@
///
/// 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 { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { WidgetTypeDetails } from '@shared/models/widget.models';
@Component({
selector: 'tb-widget-type-tabs',
templateUrl: './widget-type-tabs.component.html',
styleUrls: []
})
export class WidgetTypeTabsComponent extends EntityTabsComponent<WidgetTypeDetails> {
constructor(protected store: Store<AppState>) {
super(store);
}
isTenantWidgetType() {
return this.entity && this.entity.tenantId.id !== NULL_UUID;
}
ngOnInit() {
super.ngOnInit();
}
}

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

@ -0,0 +1,66 @@
<!--
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.
-->
<div class="tb-details-buttons" fxLayout.xs="column">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'edit')"
[fxShow]="!isEdit">
{{'widget.edit-widget-type' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[fxShow]="!isEdit">
{{'widget-type.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[fxShow]="!hideDelete() && !isEdit">
{{'widget.delete-widget-type' | translate }}
</button>
</div>
<div class="mat-padding" fxLayout="column">
<form [formGroup]="entityForm">
<fieldset [disabled]="(isLoading$ | async) || !isEdit">
<mat-form-field class="mat-block">
<mat-label translate>widget.title</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="entityForm.get('name').hasError('required')">
{{ 'widget.title-required' | translate }}
</mat-error>
<mat-error *ngIf="entityForm.get('name').hasError('maxlength')">
{{ 'widget.title-max-length' | translate }}
</mat-error>
</mat-form-field>
<tb-image-input fxFlex
label="{{'widget.image-preview' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
<mat-form-field class="mat-block">
<mat-label translate>widget.description</mat-label>
<textarea matInput formControlName="description" rows="2" maxlength="255" #descriptionInput></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint>
</mat-form-field>
<mat-slide-toggle formControlName="deprecated">
{{ 'widget.deprecated' | translate }}
</mat-slide-toggle>
</fieldset>
</form>
</div>

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

@ -0,0 +1,68 @@
///
/// 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 { ChangeDetectorRef, Component, Inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityComponent } from '../../components/entity/entity.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { EntityTableConfig } from '@home/models/entity/entities-table-config.models';
import { WidgetTypeDetails } from '@shared/models/widget.models';
@Component({
selector: 'tb-widget-type',
templateUrl: './widget-type.component.html',
styleUrls: []
})
export class WidgetTypeComponent extends EntityComponent<WidgetTypeDetails> {
constructor(protected store: Store<AppState>,
@Inject('entity') protected entityValue: WidgetTypeDetails,
@Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetTypeDetails>,
public fb: UntypedFormBuilder,
protected cd: ChangeDetectorRef) {
super(store, fb, entityValue, entitiesTableConfigValue, cd);
}
hideDelete() {
if (this.entitiesTableConfig) {
return !this.entitiesTableConfig.deleteEnabled(this.entity);
} else {
return false;
}
}
buildForm(entity: WidgetTypeDetails): UntypedFormGroup {
return this.fb.group(
{
name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]],
image: [entity ? entity.image : ''],
description: [entity ? entity.description : '', Validators.maxLength(255)],
deprecated: [entity ? entity.deprecated : false]
}
);
}
updateForm(entity: WidgetTypeDetails) {
this.entityForm.patchValue({
name: entity.name,
image: entity.image,
description: entity.description,
deprecated: entity.deprecated
});
}
}

232
ui-ngx/src/app/modules/home/pages/widget/widget-types-table-config.resolver.ts

@ -0,0 +1,232 @@
///
/// 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 { Injectable } from '@angular/core';
import { Resolve, Router } from '@angular/router';
import {
checkBoxCell,
DateEntityTableColumn,
EntityTableColumn,
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { WidgetService } from '@app/core/http/widget.service';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { getCurrentAuthState, getCurrentAuthUser } from '@app/core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { Direction } from '@shared/models/page/sort-order';
import {
BaseWidgetType,
WidgetTypeDetails,
WidgetTypeInfo,
widgetType as WidgetDataType,
widgetTypesData
} from '@shared/models/widget.models';
import { WidgetTypeComponent } from '@home/pages/widget/widget-type.component';
import { WidgetTypeTabsComponent } from '@home/pages/widget/widget-type-tabs.component';
import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
import { MatDialog } from '@angular/material/dialog';
@Injectable()
export class WidgetTypesTableConfigResolver implements Resolve<EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails>> {
private readonly config: EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails> =
new EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails>();
constructor(private store: Store<AppState>,
private dialog: MatDialog,
private widgetsService: WidgetService,
private translate: TranslateService,
private importExport: ImportExportService,
private datePipe: DatePipe,
private router: Router) {
this.config.entityType = EntityType.WIDGETS_BUNDLE;
this.config.entityComponent = WidgetTypeComponent;
this.config.entityTabsComponent = WidgetTypeTabsComponent;
this.config.entityTranslations = entityTypeTranslations.get(EntityType.WIDGET_TYPE);
this.config.entityResources = entityTypeResources.get(EntityType.WIDGET_TYPE);
this.config.defaultSortOrder = {property: 'name', direction: Direction.ASC};
this.config.rowPointer = true;
this.config.entityTitle = (widgetType) => widgetType ?
widgetType.name : '';
this.config.columns.push(
new DateEntityTableColumn<WidgetTypeInfo>('createdTime', 'common.created-time', this.datePipe, '150px'),
new EntityTableColumn<WidgetTypeInfo>('name', 'widget.title', '100%'),
new EntityTableColumn<WidgetTypeInfo>('widgetType', 'widget.type', '150px', entity =>
entity?.widgetType ? this.translate.instant(widgetTypesData.get(entity.widgetType).name) : '', undefined, false),
new EntityTableColumn<WidgetTypeInfo>('tenantId', 'widget.system', '60px',
entity => checkBoxCell(entity.tenantId.id === NULL_UUID)),
new EntityTableColumn<WidgetTypeInfo>('deprecated', 'widget.deprecated', '60px',
entity => checkBoxCell(entity.deprecated))
);
this.config.addActionDescriptors.push(
{
name: this.translate.instant('widget-type.create-new-widget-type'),
icon: 'insert_drive_file',
isEnabled: () => true,
onAction: ($event) => this.addWidgetType($event)
},
{
name: this.translate.instant('widget-type.import'),
icon: 'file_upload',
isEnabled: () => true,
onAction: ($event) => this.importWidgetType($event)
}
);
this.config.cellActionDescriptors.push(
{
name: this.translate.instant('widget-type.export'),
icon: 'file_download',
isEnabled: () => true,
onAction: ($event, entity) => this.exportWidgetType($event, entity)
},
{
name: this.translate.instant('widget.widget-type-details'),
icon: 'edit',
isEnabled: () => true,
onAction: ($event, entity) => this.config.toggleEntityDetails($event, entity)
}
);
this.config.groupActionDescriptors.push(
{
name: this.translate.instant('widget-type.export-widget-types'),
icon: 'file_download',
isEnabled: true,
onAction: ($event, entities) => this.exportWidgetTypes($event, entities)
}
);
this.config.deleteEntityTitle = widgetType => this.translate.instant('widget.delete-widget-type-title',
{ widgetTypeName: widgetType.name });
this.config.deleteEntityContent = () => this.translate.instant('widget.delete-widget-type-text');
this.config.deleteEntitiesTitle = count => this.translate.instant('widget.delete-widget-types-title', {count});
this.config.deleteEntitiesContent = () => this.translate.instant('widget.delete-widget-types-text');
this.config.loadEntity = id => this.widgetsService.getWidgetTypeById(id.id);
this.config.saveEntity = widgetType => this.widgetsService.saveWidgetType(widgetType as WidgetTypeDetails);
this.config.deleteEntity = id => this.widgetsService.deleteWidgetType(id.id);
this.config.onEntityAction = action => this.onWidgetTypeAction(action);
this.config.handleRowClick = ($event, widgetType) => {
if (this.config.isDetailsOpen()) {
this.config.toggleEntityDetails($event, widgetType);
} else {
this.openWidgetEditor($event, widgetType);
}
return true;
};
}
resolve(): EntityTableConfig<WidgetTypeInfo | WidgetTypeDetails> {
this.config.tableTitle = this.translate.instant('widget.widget-types');
const authUser = getCurrentAuthUser(this.store);
this.config.deleteEnabled = (widgetType) => this.isWidgetTypeEditable(widgetType, authUser.authority);
this.config.entitySelectionEnabled = (widgetType) => this.isWidgetTypeEditable(widgetType, authUser.authority);
this.config.detailsReadonly = (widgetType) => !this.isWidgetTypeEditable(widgetType, authUser.authority);
const authState = getCurrentAuthState(this.store);
this.config.entitiesFetchFunction = pageLink => this.widgetsService.getWidgetTypes(pageLink);
return this.config;
}
isWidgetTypeEditable(widgetType: BaseWidgetType, authority: Authority): boolean {
if (authority === Authority.TENANT_ADMIN) {
return widgetType && widgetType.tenantId && widgetType.tenantId.id !== NULL_UUID;
} else {
return authority === Authority.SYS_ADMIN;
}
}
addWidgetType($event: Event): void {
if ($event) {
$event.stopPropagation();
}
this.dialog.open<SelectWidgetTypeDialogComponent, any,
WidgetDataType>(SelectWidgetTypeDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog']
}).afterClosed().subscribe(
(type) => {
if (type) {
this.router.navigateByUrl(`resources/widgets-library/widget-types/${type}`);
}
}
);
}
importWidgetType($event: Event) {
this.importExport.importWidgetType().subscribe(
(widgetType) => {
if (widgetType) {
this.config.updateData();
}
}
);
}
openWidgetEditor($event: Event, widgetType: BaseWidgetType) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`);
}
exportWidgetType($event: Event, widgetType: BaseWidgetType) {
if ($event) {
$event.stopPropagation();
}
this.importExport.exportWidgetType(widgetType.id.id);
}
exportWidgetTypes($event: Event, widgetTypes: BaseWidgetType[]) {
if ($event) {
$event.stopPropagation();
}
this.importExport.exportWidgetTypes(widgetTypes.map(w => w.id.id)).subscribe(
() => {
this.config.getTable().clearSelection();
}
);
}
onWidgetTypeAction(action: EntityAction<BaseWidgetType>): boolean {
switch (action.action) {
case 'edit':
this.openWidgetEditor(action.event, action.entity);
return true;
case 'export':
this.exportWidgetType(action.event, action.entity);
return true;
}
return false;
}
}

134
ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.html

@ -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.
-->
<mat-card appearance="outlined" class="tb-bundle-widgets-card">
<mat-card-header>
<mat-card-title>
<div class="title-container">
<button mat-icon-button
matTooltip="{{ 'action.back' | translate }}"
matTooltipPosition="above"
(click)="goBack()">
<mat-icon>arrow_back</mat-icon>
</button>
<span class="mat-headline-5">{{ widgetsBundle.title }}: {{ 'widget.widgets' | translate }}</span>
<span fxFlex></span>
<button mat-icon-button
matTooltip="{{ 'widgets-bundle.export' | translate }}"
matTooltipPosition="above"
(click)="exportWidgetsBundle()">
<mat-icon>file_download</mat-icon>
</button>
</div>
</mat-card-title>
</mat-card-header>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div>
<mat-card-content>
<div class="tb-drop-list tb-bundle-widgets-container" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="widgetDrop($event)" [cdkDropListDisabled]="!editMode || widgets?.length < 2">
<div cdkDrag class="tb-draggable tb-bundle-widget-row" *ngFor="let widget of widgets; trackBy: trackByWidget;
let $index = index; last as isLast;">
<div class="tb-bundle-widget-row-details">
<img class="tb-bundle-widget-image-preview" [src]="getPreviewImage(widget.image)" alt="{{ widget.name }}">
<div class="tb-bundle-widget-details">
<div class="tb-bundle-widget-title">{{ widget.name }}</div>
<div *ngIf="widget.deprecated" class="tb-bundle-widget-deprecated">{{ 'widget.deprecated' | translate }}</div>
</div>
</div>
<button *ngIf="!editMode"
mat-icon-button
(click)="openWidgetEditor($event, widget)"
matTooltip="{{'widget.edit-widget-type' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button *ngIf="!editMode"
mat-icon-button
(click)="exportWidgetType($event, widget)"
matTooltip="{{'widget-type.export' | translate }}"
matTooltipPosition="above">
<mat-icon>file_download</mat-icon>
</button>
<button *ngIf="editMode && !addMode"
mat-icon-button
(click)="removeWidgetType($event, widget)"
matTooltip="{{'widget.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button cdkDragHandle *ngIf="editMode"
[fxShow]="widgets?.length > 1"
mat-icon-button
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above"
class="tb-drag-handle">
<tb-icon>drag_indicator</tb-icon>
</button>
</div>
</div>
<div *ngIf="editMode && !addMode && widgets?.length" class="tb-add-widget-button"
matTooltip="{{ 'widget.add' | translate }}"
matTooltipPosition="above"
(click)="addWidgetMode()">
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon>
</div>
<div *ngIf="editMode && !addMode && !widgets?.length" class="tb-add-widget-button-panel">
<div class="tb-add-widget-button-with-text"
matTooltip="{{ 'widget.add' | translate }}"
matTooltipPosition="above"
(click)="addWidgetMode()">
<tb-icon color="primary" class="tb-add-widget-icon">add</tb-icon>
<div class="tb-add-widget-button-text" translate>widget.add</div>
</div>
</div>
<div *ngIf="isReadOnly && !widgets?.length" class="tb-no-data-available">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text" translate>widget.no-widgets</div>
</div>
<div *ngIf="addMode" class="tb-add-bundle-widget-form">
<tb-widget-type-autocomplete
required
placeholder="{{ 'widget.select-widget' | translate }}"
[excludeWidgetTypeIds]="excludeWidgetTypeIds"
[formControl]="addWidgetFormControl">
</tb-widget-type-autocomplete>
<div class="tb-add-bundle-widget-actions">
<button mat-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="cancelAddWidgetMode()">{{'action.cancel' | translate}}
</button>
</div>
</div>
</mat-card-content>
<mat-card-actions>
<button mat-button color="primary" *ngIf="editMode && (widgets?.length || isDirty)"
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{'action.cancel' | translate}}
</button>
<button mat-raised-button color="primary" *ngIf="editMode && (widgets?.length || isDirty)"
[disabled]="(isLoading$ | async) || !isDirty"
(click)="save()">{{'action.save' | translate}}
</button>
<button mat-raised-button color="primary" *ngIf="!isReadOnly && !editMode"
[disabled]="(isLoading$ | async)"
(click)="edit()">{{'action.edit' | translate}}
</button>
</mat-card-actions>
</mat-card>

184
ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.scss

@ -0,0 +1,184 @@
/**
* 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 "../../../../../scss/constants";
:host {
width: 100%;
height: 100%;
padding: 8px;
}
:host ::ng-deep {
.tb-bundle-widgets-card {
.mat-mdc-card-header-text {
flex: 1;
}
}
}
.tb-bundle-widgets-card {
padding: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
@media #{$mat-md} {
width: 80%;
}
@media #{$mat-gt-md} {
width: 60%;
}
.title-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
.mat-headline-5 {
margin: 0;
}
}
.mat-mdc-card-content {
flex: 1;
padding: 16px;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 8px;
}
.tb-add-widget-button {
min-height: 56px;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #FFFFFF;
padding: 8px;
border: 2px dashed rgba(0, 0, 0, 0.08);
border-radius: 10px;
}
.tb-add-widget-button-panel {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.tb-add-widget-button-with-text {
cursor: pointer;
display: flex;
padding: 24px 40px 24px 24px;
justify-content: center;
align-items: center;
gap: 8px;
border: 2px dashed rgba(0, 0, 0, 0.12);
border-radius: 10px;
.tb-add-widget-button-text {
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: $tb-primary-color;
}
}
}
.tb-add-bundle-widget-form {
display: flex;
padding: 16px;
flex-direction: column;
gap: 12px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04);
}
.tb-add-bundle-widget-actions {
display: flex;
justify-content: flex-end;
align-items: flex-start;
gap: 16px;
}
.mat-mdc-card-actions {
justify-content: end;
gap: 8px;
}
}
.tb-bundle-widgets-container {
display: flex;
flex-direction: column;
gap: 8px;
min-height: 0;
overflow: auto;
padding-top: 8px;
padding-bottom: 8px;
}
.tb-bundle-widget-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 6px 12px 6px 6px;
background: #fff;
gap: 12px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: 0 5px 16px 0 rgba(0, 0, 0, 0.04);
.tb-bundle-widget-row-details {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
.tb-bundle-widget-image-preview {
width: 64px;
max-height: 100%;
object-fit: contain;
border-radius: 6px;
}
.tb-bundle-widget-details {
padding: 18px 0;
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
.tb-bundle-widget-title {
font-size: 14px;
font-weight: 400;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
}
.tb-bundle-widget-deprecated {
font-size: 13px;
font-weight: 400;
color: rgba(0, 0, 0, 0.54);
}
}
}
.mat-icon {
color: rgba(0, 0, 0, 0.54);
}
}

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

@ -0,0 +1,178 @@
///
/// 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 { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { AuthUser } from '@shared/models/user.model';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { ActivatedRoute, Router } from '@angular/router';
import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { Authority } from '@shared/models/authority.enum';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { WidgetTypeInfo } from '@shared/models/widget.models';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { ImportExportService } from '@home/components/import-export/import-export.service';
import { WidgetService } from '@core/http/widget.service';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { isDefinedAndNotNull } from '@core/utils';
import { FormControl, Validators } from '@angular/forms';
@Component({
selector: 'tb-widgets-bundle-widget',
templateUrl: './widgets-bundle-widgets.component.html',
styleUrls: ['./widgets-bundle-widgets.component.scss']
})
export class WidgetsBundleWidgetsComponent extends PageComponent implements OnInit {
authUser: AuthUser;
isReadOnly: boolean;
editMode = false;
addMode = false;
isDirty = false;
widgetsBundle: WidgetsBundle;
widgets: Array<WidgetTypeInfo>;
excludeWidgetTypeIds: Array<string>;
addWidgetFormControl = new FormControl(null, [Validators.required]);
constructor(protected store: Store<AppState>,
private router: Router,
private route: ActivatedRoute,
private widgetsService: WidgetService,
private importExport: ImportExportService,
private sanitizer: DomSanitizer,
private cd: ChangeDetectorRef) {
super(store);
this.authUser = getCurrentAuthUser(this.store);
this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
this.widgets = [...this.route.snapshot.data.widgets];
if (this.authUser.authority === Authority.TENANT_ADMIN) {
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
} else {
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
}
if (!this.isReadOnly && !this.widgets.length) {
this.editMode = true;
}
this.addWidgetFormControl.valueChanges.subscribe((newWidget) => {
if (newWidget) {
this.addWidget(newWidget);
}
});
}
ngOnInit(): void {
}
getPreviewImage(imageUrl: string | null): SafeUrl | string {
if (isDefinedAndNotNull(imageUrl)) {
return this.sanitizer.bypassSecurityTrustUrl(imageUrl);
}
return '/assets/widget-preview-empty.svg';
}
trackByWidget(index: number, widget: WidgetTypeInfo): any {
return widget;
}
widgetDrop(event: CdkDragDrop<string[]>) {
const widget = this.widgets[event.previousIndex];
this.widgets.splice(event.previousIndex, 1);
this.widgets.splice(event.currentIndex, 0, widget);
this.isDirty = true;
}
addWidgetMode() {
this.addWidgetFormControl.patchValue(null, {emitEvent: false});
this.excludeWidgetTypeIds = this.widgets.map(w => w.id.id);
this.addMode = true;
}
cancelAddWidgetMode() {
this.addMode = false;
}
private addWidget(newWidget: WidgetTypeInfo) {
this.widgets.push(newWidget);
this.isDirty = true;
this.addMode = false;
}
openWidgetEditor($event: Event, widgetType: WidgetTypeInfo) {
if ($event) {
$event.stopPropagation();
}
this.router.navigateByUrl(`resources/widgets-library/widget-types/${widgetType.id.id}`);
}
exportWidgetType($event: Event, widgetType: WidgetTypeInfo) {
if ($event) {
$event.stopPropagation();
}
this.importExport.exportWidgetType(widgetType.id.id);
}
removeWidgetType($event: Event, widgetType: WidgetTypeInfo) {
if ($event) {
$event.stopPropagation();
}
const index = this.widgets.indexOf(widgetType);
this.widgets.splice(index, 1);
this.isDirty = true;
}
goBack() {
this.router.navigate(['..'], { relativeTo: this.route });
}
exportWidgetsBundle() {
this.importExport.exportWidgetsBundle(this.widgetsBundle.id.id);
}
edit() {
this.editMode = true;
}
cancel() {
if (this.isDirty) {
this.widgetsService.getBundleWidgetTypeInfos(this.widgetsBundle.id.id).subscribe(
(widgets) => {
this.widgets = [...widgets];
this.isDirty = false;
this.addMode = false;
this.editMode = !this.widgets.length;
this.cd.markForCheck();
}
);
} else {
this.addMode = false;
this.editMode = !this.widgets.length;
}
}
save() {
const widgetTypeIds = this.widgets.map(w => w.id.id);
this.widgetsService.updateWidgetsBundleWidgetTypes(this.widgetsBundle.id.id, widgetTypeIds).subscribe(() => {
this.isDirty = false;
this.editMode = false;
});
}
}

4
ui-ngx/src/app/modules/home/pages/widget/widgets-bundles-table-config.resolver.ts

@ -125,6 +125,10 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
} }
return true; return true;
}; };
this.config.entityAdded = widgetsBundle => {
this.openWidgetsBundle(null, widgetsBundle);
};
} }
resolve(): EntityTableConfig<WidgetsBundle> { resolve(): EntityTableConfig<WidgetsBundle> {

3
ui-ngx/src/app/shared/models/constants.ts

@ -149,7 +149,8 @@ export const HelpLinks = {
lwm2mResourceLibrary: helpBaseUrl + '/docs/reference/lwm2m-api', lwm2mResourceLibrary: helpBaseUrl + '/docs/reference/lwm2m-api',
dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
otaUpdates: helpBaseUrl + '/docs/user-guide/ota-updates', otaUpdates: helpBaseUrl + '/docs/user-guide/ota-updates',
widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles', widgetTypes: helpBaseUrl + '/docs/user-guide/ui/widget-library/#widget-types',
widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library/#widgets-library-bundles',
widgetsConfig: helpBaseUrl + '/docs/user-guide/ui/dashboards#widget-configuration', widgetsConfig: helpBaseUrl + '/docs/user-guide/ui/dashboards#widget-configuration',
widgetsConfigTimeseries: helpBaseUrl + '/docs/user-guide/ui/dashboards#timeseries', widgetsConfigTimeseries: helpBaseUrl + '/docs/user-guide/ui/dashboards#timeseries',
widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest', widgetsConfigLatest: helpBaseUrl + '/docs/user-guide/ui/dashboards#latest',

19
ui-ngx/src/app/shared/models/entity-type.models.ts

@ -268,6 +268,19 @@ export const entityTypeTranslations = new Map<EntityType | AliasEntityType, Enti
type: 'entity.type-api-usage-state' type: 'entity.type-api-usage-state'
} }
], ],
[
EntityType.WIDGET_TYPE,
{
type: 'entity.type-widget-type',
typePlural: 'entity.type-widget-types',
list: 'entity.list-of-widget-types',
details: 'widget.details',
add: 'widget.add-widget-type',
noEntities: 'widget.no-widget-types-text',
search: 'widget.search-widget-types',
selectedEntities: 'widget.selected-widget-types'
}
],
[ [
EntityType.WIDGETS_BUNDLE, EntityType.WIDGETS_BUNDLE,
{ {
@ -474,6 +487,12 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
helpLinkId: 'dashboards' helpLinkId: 'dashboards'
} }
], ],
[
EntityType.WIDGET_TYPE,
{
helpLinkId: 'widgetTypes'
}
],
[ [
EntityType.WIDGETS_BUNDLE, EntityType.WIDGETS_BUNDLE,
{ {

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

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { BaseData } from '@shared/models/base-data'; import { BaseData, ExportableEntity } from '@shared/models/base-data';
import { TenantId } from '@shared/models/id/tenant-id'; import { TenantId } from '@shared/models/id/tenant-id';
import { WidgetTypeId } from '@shared/models/id/widget-type-id'; import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/time/time.models'; import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/time/time.models';
@ -192,7 +192,6 @@ export interface WidgetControllerDescriptor {
export interface BaseWidgetType extends BaseData<WidgetTypeId> { export interface BaseWidgetType extends BaseData<WidgetTypeId> {
tenantId: TenantId; tenantId: TenantId;
bundleAlias: string;
fqn: string; fqn: string;
name: string; name: string;
deprecated: boolean; deprecated: boolean;
@ -235,7 +234,7 @@ export interface WidgetTypeInfo extends BaseWidgetType {
widgetType: widgetType; widgetType: widgetType;
} }
export interface WidgetTypeDetails extends WidgetType { export interface WidgetTypeDetails extends WidgetType, ExportableEntity<WidgetTypeId> {
image: string; image: string;
description: string; description: string;
} }

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

@ -733,7 +733,6 @@
"no-attributes-text": "No attributes found", "no-attributes-text": "No attributes found",
"no-telemetry-text": "No telemetry found", "no-telemetry-text": "No telemetry found",
"copy-key": "Copy key", "copy-key": "Copy key",
"copy-value": "Copy value",
"add-telemetry": "Add telemetry", "add-telemetry": "Add telemetry",
"copy-value": "Copy value", "copy-value": "Copy value",
"delete-timeseries": { "delete-timeseries": {
@ -2148,6 +2147,9 @@
"type-widgets-bundle": "Widgets bundle", "type-widgets-bundle": "Widgets bundle",
"type-widgets-bundles": "Widgets bundles", "type-widgets-bundles": "Widgets bundles",
"list-of-widgets-bundles": "{ count, plural, =1 {One widgets bundle} other {List of # widget bundles} }", "list-of-widgets-bundles": "{ count, plural, =1 {One widgets bundle} other {List of # widget bundles} }",
"type-widget-type": "Widget type",
"type-widget-types": "Widget types",
"list-of-widget-types": "{ count, plural, =1 {One widget type} other {List of # widget types} }",
"search": "Search entities", "search": "Search entities",
"selected-entities": "{ count, plural, =1 {1 entity} other {# entities} } selected", "selected-entities": "{ count, plural, =1 {1 entity} other {# entities} } selected",
"entity-name": "Entity name", "entity-name": "Entity name",
@ -4528,12 +4530,19 @@
"widget-bundle": "Widgets Bundle", "widget-bundle": "Widgets Bundle",
"all-bundles": "All bundles", "all-bundles": "All bundles",
"select-widgets-bundle": "Select widgets bundle", "select-widgets-bundle": "Select widgets bundle",
"widgets": "Widgets",
"widget": "Widget",
"select-widget": "Select widget",
"no-widgets-matching": "No widgets matching '{{entity}}' were found.",
"no-widgets": "No widgets yet",
"management": "Widget management", "management": "Widget management",
"editor": "Widget Editor", "editor": "Widget Editor",
"widget-type-not-found": "Problem loading widget configuration.<br>Probably associated\n widget type was removed.", "widget-type-not-found": "Problem loading widget configuration.<br>Probably associated\n widget type was removed.",
"widget-type-load-error": "Widget wasn't loaded due to the following errors:", "widget-type-load-error": "Widget wasn't loaded due to the following errors:",
"remove": "Remove widget", "remove": "Remove widget",
"delete": "Delete widget",
"edit": "Edit widget", "edit": "Edit widget",
"edit-widget-type": "Edit widget type",
"remove-widget-title": "Are you sure you want to remove the widget '{{widgetTitle}}'?", "remove-widget-title": "Are you sure you want to remove the widget '{{widgetTitle}}'?",
"remove-widget-text": "After the confirmation the widget and all related data will become unrecoverable.", "remove-widget-text": "After the confirmation the widget and all related data will become unrecoverable.",
"timeseries": "Time series", "timeseries": "Time series",
@ -4551,13 +4560,14 @@
"saveAs": "Save widget as", "saveAs": "Save widget as",
"move": "Move widget", "move": "Move widget",
"save-widget-type-as": "Save widget type as", "save-widget-type-as": "Save widget type as",
"save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle", "save-widget-type-as-text": "Please enter new widget title",
"move-widget-type": "Move widget type",
"move-widget-type-text": "Please select target widgets bundle",
"toggle-fullscreen": "Toggle fullscreen", "toggle-fullscreen": "Toggle fullscreen",
"run": "Run widget", "run": "Run widget",
"title": "Widget title", "widget-title": "Widget title",
"title": "Title",
"title-required": "Widget title is required.", "title-required": "Widget title is required.",
"title-max-length": "Title should be less than 256",
"system": "System",
"type": "Widget type", "type": "Widget type",
"resources": "Resources", "resources": "Resources",
"resource-url": "JavaScript/CSS URL", "resource-url": "JavaScript/CSS URL",
@ -4588,10 +4598,21 @@
"remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?",
"remove-widget-type-text": "After the confirmation the widget type and all related data will become unrecoverable.", "remove-widget-type-text": "After the confirmation the widget type and all related data will become unrecoverable.",
"remove-widget-type": "Remove widget type", "remove-widget-type": "Remove widget type",
"widget-types": "Widget Types",
"delete-widget-type-title": "Are you sure you want to delete the widget type '{{widgetTypeName}}'?",
"delete-widget-type-text": "After the confirmation the widget and all related data will become unrecoverable.",
"delete-widget-types-title": "Are you sure you want to delete { count, plural, =1 {1 widget type} other {# widget types} }?",
"delete-widget-types-text": "Be careful, after the confirmation all selected widget types will be removed and all related data will become unrecoverable.",
"delete-widget-type": "Delete widget type",
"add-widget-type": "Add new widget type", "add-widget-type": "Add new widget type",
"widget-type-load-failed-error": "Failed to load widget type!", "widget-type-load-failed-error": "Failed to load widget type!",
"widget-template-load-failed-error": "Failed to load widget template!", "widget-template-load-failed-error": "Failed to load widget template!",
"details": "Details",
"widget-type-details": "Widget type details",
"add": "Add Widget", "add": "Add Widget",
"no-widget-types-text": "No widget types found",
"search-widget-types": "Search widget types",
"selected-widget-types": "{ count, plural, =1 {1 widget type} other {# widget types} } selected",
"undo": "Undo widget changes", "undo": "Undo widget changes",
"export": "Export widget", "export": "Export widget",
"no-data": "No data to display on widget", "no-data": "No data to display on widget",
@ -4662,6 +4683,7 @@
"widgets-bundle": { "widgets-bundle": {
"current": "Current bundle", "current": "Current bundle",
"widgets-bundles": "Widgets Bundles", "widgets-bundles": "Widgets Bundles",
"widgets-bundle-widgets": "Widgets Bundle Widgets",
"add": "Add Widgets Bundle", "add": "Add Widgets Bundle",
"delete": "Delete widgets bundle", "delete": "Delete widgets bundle",
"title": "Title", "title": "Title",
@ -4684,6 +4706,7 @@
"system": "System", "system": "System",
"import": "Import widgets bundle", "import": "Import widgets bundle",
"export": "Export widgets bundle", "export": "Export widgets bundle",
"export-widgets-bundle-widgets-prompt": "Include bundle widget types in exported data (otherwise only referenced widget type FQNs will be exported)",
"export-failed-error": "Unable to export widgets bundle: {{error}}", "export-failed-error": "Unable to export widgets bundle: {{error}}",
"create-new-widgets-bundle": "Create new widgets bundle", "create-new-widgets-bundle": "Create new widgets bundle",
"widgets-bundle-file": "Widgets bundle file", "widgets-bundle-file": "Widgets bundle file",
@ -4790,6 +4813,7 @@
"widget-type": { "widget-type": {
"import": "Import widget type", "import": "Import widget type",
"export": "Export widget type", "export": "Export widget type",
"export-widget-types": "Export widget types",
"export-failed-error": "Unable to export widget type: {{error}}", "export-failed-error": "Unable to export widget type: {{error}}",
"create-new-widget-type": "Create new widget type", "create-new-widget-type": "Create new widget type",
"widget-type-file": "Widget type file", "widget-type-file": "Widget type file",

Loading…
Cancel
Save