Browse Source

Widget Bundles to Wudget Types Many to Many support for UI

feature/widget-bundles
Igor Kulikov 2 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. ";
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 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)",
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);
}
@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)",
notes = "Create or update the Widget Type. " + WIDGET_TYPE_DESCRIPTION + " " +
"When creating the Widget Type, platform generates Widget Type Id as " + UUID_WIKI_LINK +
@ -103,7 +117,9 @@ public class WidgetTypeController extends AutoCommitController {
@ResponseBody
public WidgetTypeDetails saveWidgetType(
@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();
if (Authority.SYS_ADMIN.equals(currentUser.getAuthority())) {
widgetTypeDetails.setTenantId(TenantId.SYS_TENANT_ID);
@ -112,7 +128,7 @@ public class WidgetTypeController extends AutoCommitController {
}
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)",
@ -224,6 +240,18 @@ public class WidgetTypeController extends AutoCommitController {
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)",
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')")

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);
}
@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)",
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')")

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);
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 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.audit.ActionType;
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.dao.widget.WidgetTypeService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -34,9 +35,20 @@ public class DefaultWidgetTypeService extends AbstractTbEntityService implements
private final WidgetTypeService widgetTypeService;
@Override
public WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, User user) throws Exception {
ActionType actionType = widgetTypeDetails.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
public WidgetTypeDetails save(WidgetTypeDetails entity, User user) throws Exception {
return this.save(entity, false, user);
}
@Override
public WidgetTypeDetails save(WidgetTypeDetails widgetTypeDetails, boolean updateExistingByFqn, User user) throws Exception {
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 {
WidgetTypeDetails savedWidgetTypeDetails = checkNotNull(widgetTypeService.saveWidgetType(widgetTypeDetails));
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;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
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.timeseries.TimeseriesService;
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.service.security.auth.jwt.settings.JwtSettingsService;
@ -136,6 +137,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
private WidgetTypeService widgetTypeService;
@Autowired
private WidgetsBundleService widgetsBundleService;
@ -485,6 +489,10 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
public void deleteSystemWidgetBundle(String bundleAlias) throws Exception {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) {
var widgetTypes = widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
for (var widgetType : widgetTypes) {
widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetType.getId());
}
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.id.CustomerId;
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.rule.RuleChain;
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.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Optional;
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 DEMO_DIR = "demo";
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 OAUTH2_CONFIG_TEMPLATES_DIR = "oauth2_config_templates";
public static final String DASHBOARDS_DIR = "dashboards";
@ -180,6 +184,23 @@ public class InstallScripts {
}
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);
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
dirStream.forEach(
@ -189,19 +210,29 @@ public class InstallScripts {
JsonNode widgetsBundleJson = widgetsBundleDescriptorJson.get("widgetsBundle");
WidgetsBundle widgetsBundle = JacksonUtil.treeToValue(widgetsBundleJson, WidgetsBundle.class);
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
widgetTypesArrayJson.forEach(
widgetTypeJson -> {
try {
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
// widgetTypeDetails.setBundleAlias(savedWidgetsBundle.getAlias()); // TODO:
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);
List<String> widgetTypeFqns = new ArrayList<>();
if (widgetsBundleDescriptorJson.has("widgetTypes")) {
JsonNode widgetTypesArrayJson = widgetsBundleDescriptorJson.get("widgetTypes");
widgetTypesArrayJson.forEach(
widgetTypeJson -> {
try {
WidgetTypeDetails widgetTypeDetails = JacksonUtil.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
var savedWidgetType = widgetTypeService.saveWidgetType(widgetTypeDetails);
widgetTypeFqns.add(savedWidgetType.getFqn());
} 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) {
log.error("Unable to load widgets bundle from json: [{}]", path.toString());
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.template.NotificationTemplate;
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 java.lang.annotation.ElementType;
@ -52,6 +53,7 @@ import java.lang.annotation.Target;
@Type(name = "CUSTOMER", value = Customer.class),
@Type(name = "ENTITY_VIEW", value = EntityView.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_TARGET", value = NotificationTarget.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.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
* limitations under the License.
*/
:host {
button.tb-add-new-widget {
height: auto;
padding-right: 12px;
font-size: 24px;
border-style: dashed;
border-width: 2px;
}
}
package org.thingsboard.server.dao.model.sql;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.UUID;
:host ::ng-deep {
.tb-widget-library {
.tb-widget-container {
cursor: pointer;
}
}
@Data
@AllArgsConstructor
public class WidgetTypeIdFqnEntity {
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.widget.WidgetTypeDao;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@ -139,8 +140,10 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
@Override
public List<WidgetTypeId> findWidgetTypeIdsByTenantIdAndFqns(UUID tenantId, List<String> widgetFqns) {
return widgetTypeRepository.findWidgetTypeIdsByTenantIdAndFqns(tenantId, widgetFqns).stream()
.map(WidgetTypeId::new).collect(Collectors.toList());
var idFqnPairs = widgetTypeRepository.findWidgetTypeIdsByTenantIdAndFqns(tenantId, widgetFqns);
idFqnPairs.sort(Comparator.comparingInt(o -> widgetFqns.indexOf(o.getFqn())));
return idFqnPairs.stream()
.map(id -> new WidgetTypeId(id.getId())).collect(Collectors.toList());
}
@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.model.sql.WidgetTypeDetailsEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeIdFqnEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity;
import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity;
@ -81,10 +82,10 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
"AND wbw.widgetTypeId = wtd.id ORDER BY wbw.widgetTypeOrder")
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 " +
"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 " +
"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 { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import {
BaseWidgetType,
fullWidgetTypeFqn,
Widget,
WidgetType,
@ -30,9 +31,7 @@ import {
WidgetTypeInfo,
widgetTypesData
} from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
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 { filter, map, mergeMap, tap } from 'rxjs/operators';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
@ -56,8 +55,6 @@ export class WidgetService {
constructor(
private http: HttpClient,
private utils: UtilsService,
private resources: ResourcesService,
private translate: TranslateService,
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) {
return this.getWidgetsBundle(widgetsBundleId, config).pipe(
mergeMap((widgetsBundle) => this.http.delete(`/api/widgetsBundle/${widgetsBundleId}`,
defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.invalidateWidgetsBundleCache();
this.widgetsBundleDeleted(widgetsBundle);
})
)
));
}
public getBundleWidgetTypes(bundleAlias: string, isSystem: boolean,
public getBundleWidgetTypes(widgetsBundleId: string,
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));
}
public getBundleWidgetTypesDetails(bundleAlias: string, isSystem: boolean,
public getBundleWidgetTypesDetails(widgetsBundleId: string,
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));
}
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>> {
const key = bundleAlias + (isSystem ? '_sys' : '');
if (this.widgetTypeInfosCache.has(key)) {
return of(this.widgetTypeInfosCache.get(key));
if (this.widgetTypeInfosCache.has(widgetsBundleId)) {
return of(this.widgetTypeInfosCache.get(widgetsBundleId));
} 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(
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>> {
return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe(
return this.getBundleWidgetTypes(widgetsBundleId, config).pipe(
map((types) => {
types = types.sort((a, b) => {
let result = (a.deprecated ? 1 : 0) - (b.deprecated ? 1 : 0);
@ -211,10 +232,9 @@ export class WidgetService {
public saveWidgetTypeDetails(widgetInfo: WidgetInfo,
id: WidgetTypeId,
bundleAlias: string,
createdTime: number,
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,
defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => {
@ -222,46 +242,47 @@ export class WidgetService {
}));
}
public setWidgetTypeDeprecated(widgetTypeId: string, deprecated: boolean, config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}/deprecate/${deprecated}`,
public saveImportedWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails,
config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>('/api/widgetType?updateExistingByFqn=true', widgetTypeDetails,
defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => {
this.widgetTypeUpdated(savedWidgetType);
}));
}
public moveWidgetType(widgetTypeId: string, targetBundleAlias: string, config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}/move?targetBundleAlias=${targetBundleAlias}`,
defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => {
this.widgetTypeUpdated(savedWidgetType);
}));
public getWidgetTypeById(widgetTypeId: string,
config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.get<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}`,
defaultHttpOptionsFromConfig(config));
}
public saveImportedWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails,
config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails,
defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => {
this.widgetTypeUpdated(savedWidgetType);
}));
public getWidgetTypeInfoById(widgetTypeId: string,
config?: RequestConfig): Observable<WidgetTypeInfo> {
return this.http.get<WidgetTypeInfo>(`/api/widgetTypeInfo/${widgetTypeId}`,
defaultHttpOptionsFromConfig(config));
}
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) {
return this.getWidgetType(fullFqn, config).pipe(
mergeMap((widgetTypeInstance) => this.http.delete(`/api/widgetType/${widgetTypeInstance.id.id}`,
defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.widgetTypeUpdated(widgetTypeInstance);
})
)
));
return this.getWidgetTypeById(widgetTypeId, config).pipe(
mergeMap((widgetTypeDetails) =>
this.http.delete(`/api/widgetType/${widgetTypeId}`, defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.widgetTypeUpdated(widgetTypeDetails);
})
)
));
}
public getWidgetTypeById(widgetTypeId: string,
config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.get<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}`,
public getWidgetTypes(pageLink: PageLink, tenantOnly = false, config?: RequestConfig): Observable<PageData<WidgetTypeInfo>> {
return this.http.get<PageData<WidgetTypeInfo>>(`/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}`,
defaultHttpOptionsFromConfig(config));
}
@ -286,26 +307,14 @@ export class WidgetService {
this.widgetsInfoInMemoryCache.set(widgetInfo.fullFqn, widgetInfo);
}
private widgetTypeUpdated(updatedWidgetType: WidgetType): void {
private widgetTypeUpdated(updatedWidgetType: BaseWidgetType): void {
this.deleteWidgetInfoFromCache(fullWidgetTypeFqn(updatedWidgetType));
}
private widgetsBundleDeleted(widgetsBundle: WidgetsBundle): void {
this.deleteWidgetsBundleFromCache(widgetsBundle.alias);
}
public deleteWidgetInfoFromCache(fullFqn: string) {
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> {
if (!this.allWidgetsBundles) {
if (!this.loadWidgetsBundleCacheSubject) {

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

@ -124,8 +124,24 @@ export class MenuService {
id: 'widget_library',
name: 'widget.widget-library',
type: 'link',
path: '/resources/widgets-bundles',
icon: 'now_widgets'
path: '/resources/widgets-library',
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',
@ -283,7 +299,7 @@ export class MenuService {
{
name: 'widget.widget-library',
icon: 'now_widgets',
path: '/widgets-bundles'
path: '/resources/widgets-library',
}
]
},
@ -492,8 +508,24 @@ export class MenuService {
id: 'widget_library',
name: 'widget.widget-library',
type: 'link',
path: '/resources/widgets-bundles',
icon: 'now_widgets'
path: '/resources/widgets-library',
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',

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;
if (widgetsBundle) {
this.widgetsLoaded = false;
const bundleAlias = widgetsBundle.alias;
const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe(
this.widgetService.getBundleWidgetTypes(widgetsBundle.id.id).subscribe(
(widgetTypes) => {
widgetTypes = widgetTypes.sort((a, b) => {
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>> {
if (!this.widgetsInfo) {
if (this.widgetsBundle !== null) {
const bundleAlias = this.widgetsBundle.alias;
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
this.loadingWidgetsSubject.next(true);
this.widgetsInfo = this.widgetsService.getBundleWidgetTypeInfos(bundleAlias, isSystem).pipe(
this.widgetsInfo = this.widgetsService.getBundleWidgetTypeInfos(this.widgetsBundle.id.id).pipe(
map(widgets => {
widgets = widgets.sort((a, b) => b.createdTime - a.createdTime);
const widgetTypes = new Set<widgetType>();
const hasDeprecated = widgets.some(w => w.deprecated);
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 { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component';
import {
ExportWidgetsBundleDialogComponent
} from '@home/components/import-export/export-widgets-bundle-dialog.component';
@NgModule({
declarations:
@ -228,6 +231,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
CustomDialogContainerComponent,
ImportDialogComponent,
ImportDialogCsvComponent,
ExportWidgetsBundleDialogComponent,
SelectTargetLayoutDialogComponent,
SelectTargetStateDialogComponent,
AddWidgetToDashboardDialogComponent,
@ -372,6 +376,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
CustomDialogContainerComponent,
ImportDialogComponent,
ImportDialogCsvComponent,
ExportWidgetsBundleDialogComponent,
TableColumnsAssignmentComponent,
SelectTargetLayoutDialogComponent,
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 {
widgetsBundle: WidgetsBundle;
widgetTypes: WidgetTypeDetails[];
widgetTypes?: WidgetTypeDetails[];
widgetTypeFqns?: string[];
}
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';
import { MatDialog } from '@angular/material/dialog';
import { ImportDialogComponent, ImportDialogData } from '@home/components/import-export/import-dialog.component';
import { forkJoin, Observable, of } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { forkJoin, Observable, of, Subject } from 'rxjs';
import { catchError, map, mergeMap, switchMap, tap } from 'rxjs/operators';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service';
import { Widget, WidgetSize, WidgetType, WidgetTypeDetails } from '@shared/models/widget.models';
@ -58,7 +58,6 @@ import {
import { EntityType } from '@shared/models/entity-type.models';
import { UtilsService } from '@core/services/utils.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 { ImportEntitiesResultInfo, ImportEntityData } from '@shared/models/entity.models';
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 { AssetProfileService } from '@core/http/asset-profile.service';
import { AssetProfile } from '@shared/models/asset.models';
import {
ExportWidgetsBundleDialogComponent,
ExportWidgetsBundleDialogData,
ExportWidgetsBundleDialogResult
} from '@home/components/import-export/export-widgets-bundle-dialog.component';
// @dynamic
@Injectable()
@ -261,9 +265,6 @@ export class ImportExportService {
public exportWidgetType(widgetTypeId: string) {
this.widgetService.getWidgetTypeById(widgetTypeId).subscribe(
(widgetTypeDetails) => {
if (isDefined(widgetTypeDetails.bundleAlias)) {
delete widgetTypeDetails.bundleAlias;
}
let name = widgetTypeDetails.name;
name = name.toLowerCase().replace(/\W/g, '_');
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(
mergeMap((widgetTypeDetails: WidgetTypeDetails) => {
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) {
@ -283,7 +305,6 @@ export class ImportExportService {
type: 'error'}));
throw new Error('Invalid widget type file');
} else {
widgetTypeDetails.bundleAlias = bundleAlias;
return this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails);
}
}),
@ -296,27 +317,22 @@ export class ImportExportService {
public exportWidgetsBundle(widgetsBundleId: string) {
this.widgetService.getWidgetsBundle(widgetsBundleId).subscribe(
(widgetsBundle) => {
const bundleAlias = widgetsBundle.alias;
const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
this.widgetService.getBundleWidgetTypesDetails(bundleAlias, isSystem).subscribe(
(widgetTypesDetails) => {
widgetTypesDetails = widgetTypesDetails.sort((a, b) => a.createdTime - b.createdTime);
const widgetsBundleItem: WidgetsBundleItem = {
widgetsBundle: this.prepareExport(widgetsBundle),
widgetTypes: []
};
for (const widgetTypeDetails of widgetTypesDetails) {
if (isDefined(widgetTypeDetails.bundleAlias)) {
delete widgetTypeDetails.bundleAlias;
this.dialog.open<ExportWidgetsBundleDialogComponent, ExportWidgetsBundleDialogData,
ExportWidgetsBundleDialogResult>(ExportWidgetsBundleDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
widgetsBundle
}
}).afterClosed().subscribe(
(result) => {
if (result) {
if (result.exportWidgets) {
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> {
return this.openImportDialog('widgets-bundle.import', 'widgets-bundle.widgets-bundle-file').pipe(
mergeMap((widgetsBundleItem: WidgetsBundleItem) => {
@ -338,16 +391,32 @@ export class ImportExportService {
const widgetsBundle = widgetsBundleItem.widgetsBundle;
return this.widgetService.saveWidgetsBundle(widgetsBundle).pipe(
mergeMap((savedWidgetsBundle) => {
const bundleAlias = savedWidgetsBundle.alias;
const widgetTypesDetails = widgetsBundleItem.widgetTypes;
if (widgetTypesDetails.length) {
const saveWidgetTypesObservables: Array<Observable<WidgetType>> = [];
for (const widgetTypeDetails of widgetTypesDetails) {
widgetTypeDetails.bundleAlias = bundleAlias;
saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails));
if (widgetsBundleItem.widgetTypes?.length || widgetsBundleItem.widgetTypeFqns?.length) {
let widgetTypesObservable: Observable<Array<WidgetTypeDetails>>;
if (widgetsBundleItem.widgetTypes?.length) {
const widgetTypesDetails = widgetsBundleItem.widgetTypes;
const saveWidgetTypesObservables: Array<Observable<WidgetTypeDetails>> = [];
for (const widgetTypeDetails of widgetTypesDetails) {
saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails));
}
widgetTypesObservable = forkJoin(saveWidgetTypesObservables);
} else {
widgetTypesObservable = of([]);
}
return forkJoin(saveWidgetTypesObservables).pipe(
map(() => savedWidgetsBundle)
return widgetTypesObservable.pipe(
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 {
return of(savedWidgetsBundle);
@ -636,19 +705,28 @@ export class ImportExportService {
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) => {
const jsZip = new JSZip.default();
for (const keyName in data) {
if (data.hasOwnProperty(keyName)) {
const valueData = data[keyName];
jsZip.file(keyName, valueData);
try {
const jsZip = new JSZip.default();
for (const keyName in data) {
if (data.hasOwnProperty(keyName)) {
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 {
@ -765,16 +843,23 @@ export class ImportExportService {
if (isUndefined(widgetsBundleItem.widgetsBundle)) {
return false;
}
if (isUndefined(widgetsBundleItem.widgetTypes)) {
if (isUndefined(widgetsBundleItem.widgetTypes) && isUndefined(widgetsBundleItem.widgetTypeFqns)) {
return false;
}
const widgetsBundle = widgetsBundleItem.widgetsBundle;
if (isUndefined(widgetsBundle.title)) {
return false;
}
const widgetTypesDetails = widgetsBundleItem.widgetTypes;
for (const widgetTypeDetails of widgetTypesDetails) {
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) {
if (isDefined(widgetsBundleItem.widgetTypes)) {
const widgetTypesDetails = widgetsBundleItem.widgetTypes;
for (const widgetTypeDetails of widgetTypesDetails) {
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) {
return false;
}
}
}
if (isDefined(widgetsBundleItem.widgetTypeFqns)) {
if (!Array.isArray(widgetsBundleItem.widgetTypeFqns)) {
return false;
}
}

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

@ -31,7 +31,6 @@
margin-left: 0;
}
span {
overflow: hidden;
text-overflow: ellipsis;
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(
{
widgetName: this.utils.editWidgetInfo.widgetName,
bundleAlias: 'customWidgetBundle',
fullFqn: 'system.customWidget',
deprecated: false,
type: this.utils.editWidgetInfo.type,
@ -104,7 +103,7 @@ export class WidgetComponentService {
hasBasicMode: this.utils.editWidgetInfo.hasBasicMode,
basicModeDirective: this.utils.editWidgetInfo.basicModeDirective,
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>();

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 {
widgetName: string;
bundleAlias: string;
fullFqn: string;
deprecated: boolean;
typeSettingsSchema?: string | any;
@ -542,7 +541,6 @@ export interface WidgetConfigComponentData {
export const MissingWidgetType: WidgetInfo = {
type: widgetType.latest,
widgetName: 'Widget type not found',
bundleAlias: 'undefined',
fullFqn: 'undefined',
deprecated: false,
sizeX: 8,
@ -568,7 +566,6 @@ export const MissingWidgetType: WidgetInfo = {
export const ErrorWidgetType: WidgetInfo = {
type: widgetType.latest,
widgetName: 'Error loading widget',
bundleAlias: 'error',
fullFqn: 'error',
deprecated: false,
sizeX: 8,
@ -611,7 +608,6 @@ export interface WidgetTypeInstance {
export const toWidgetInfo = (widgetTypeEntity: WidgetType): WidgetInfo => ({
widgetName: widgetTypeEntity.name,
bundleAlias: widgetTypeEntity.bundleAlias,
fullFqn: fullWidgetTypeFqn(widgetTypeEntity),
deprecated: widgetTypeEntity.deprecated,
type: widgetTypeEntity.descriptor.type,
@ -640,7 +636,7 @@ export const detailsToWidgetInfo = (widgetTypeDetailsEntity: WidgetTypeDetails):
};
export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
bundleAlias: string, createdTime: number): WidgetType => {
createdTime: number): WidgetType => {
const descriptor: WidgetTypeDescriptor = {
type: widgetInfo.type,
sizeX: widgetInfo.sizeX,
@ -663,7 +659,6 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
id,
tenantId,
createdTime,
bundleAlias,
fqn: widgetTypeFqn(widgetInfo.fullFqn),
name: widgetInfo.widgetName,
deprecated: widgetInfo.deprecated,
@ -672,8 +667,8 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
};
export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
bundleAlias: string, createdTime: number): WidgetTypeDetails => {
const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, bundleAlias, createdTime);
createdTime: number): WidgetTypeDetails => {
const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime);
return {
...widgetTypeEntity,
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 { AutoCommitAdminSettingsComponent } from '@home/pages/admin/auto-commit-admin-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 { auditLogsRoutes } from '@home/pages/audit-log/audit-log-routing.module';
@ -67,10 +67,10 @@ const routes: Routes = [
children: [],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
redirectTo: '/resources/widgets-bundles'
redirectTo: '/resources/widgets-library'
}
},
...widgetsBundlesRoutes,
...widgetsLibraryRoutes,
{
path: 'resources-library',
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 }}
</mat-error>
</mat-form-field>
<tb-widgets-bundle-select fxFlex
formControlName="widgetsBundle"
required
bundlesScope="{{bundlesScope}}">
</tb-widgets-bundle-select>
</fieldset>
</div>
<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 { 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 SaveWidgetTypeAsDialogResult {
widgetName: string;
bundleId: string;
bundleAlias: string;
}
@Component({
@ -41,26 +36,16 @@ export class SaveWidgetTypeAsDialogComponent extends
saveWidgetTypeAsFormGroup: UntypedFormGroup;
bundlesScope: string;
constructor(protected store: Store<AppState>,
protected router: Router,
public dialogRef: MatDialogRef<SaveWidgetTypeAsDialogComponent, SaveWidgetTypeAsDialogResult>,
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.saveWidgetTypeAsFormGroup = this.fb.group({
title: [null, [Validators.required]],
widgetsBundle: [null, [Validators.required]]
title: [null, [Validators.required]]
});
}
@ -70,11 +55,8 @@ export class SaveWidgetTypeAsDialogComponent extends
saveAs(): void {
const widgetName: string = this.saveWidgetTypeAsFormGroup.get('title').value;
const widgetsBundle: WidgetsBundle = this.saveWidgetTypeAsFormGroup.get('widgetsBundle').value;
const result: SaveWidgetTypeAsDialogResult = {
widgetName,
bundleId: widgetsBundle.id.id,
bundleAlias: widgetsBundle.alias
widgetName
};
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">
<input [disabled]="isReadOnly" matInput required maxlength="255"
[(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true"
placeholder="{{ 'widget.title' | translate }}"/>
placeholder="{{ 'widget.widget-title' | translate }}"/>
</mat-form-field>
<mat-form-field class="tb-appearance-transparent">
<mat-select [disabled]="isReadOnly" matInput placeholder="{{ 'widget.type' | translate }}"
@ -66,15 +66,6 @@
<mat-icon>save_as</mat-icon>
<span translate>action.saveAs</span>
</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
fxHide.lt-lg
(click)="fullscreen = !fullscreen"
@ -118,12 +109,6 @@
<mat-icon>save_as</mat-icon>
<span translate>action.saveAs</span>
</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-toolbar>
<div fxFlex style="position: relative;">

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

@ -25,7 +25,6 @@ import {
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
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 { map, tap } from 'rxjs/operators';
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;
// @dynamic
@ -124,7 +118,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
isReadOnly: boolean;
widgetsBundle: WidgetsBundle;
widgetTypeDetails: WidgetTypeDetails;
widget: WidgetInfo;
origWidget: WidgetInfo;
@ -155,7 +148,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
iframeWidgetEditModeInited = false;
saveWidgetPending = false;
saveWidgetAsPending = false;
moveWidgetPending = false;
gotError = false;
errorMarkers: number[] = [];
@ -191,14 +183,13 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}
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) {
this.isReadOnly = !this.widgetsBundle || this.widgetsBundle.tenantId.id === NULL_UUID;
this.isReadOnly = this.widgetTypeDetails && this.widgetTypeDetails.tenantId.id === NULL_UUID;
} else {
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
}
this.widgetTypeDetails = data.widgetEditorData.widgetTypeDetails;
this.widget = data.widgetEditorData.widget;
if (this.widgetTypeDetails) {
const config = JSON.parse(this.widget.defaultConfig);
this.widget.defaultConfig = JSON.stringify(config);
@ -259,16 +250,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
}, ['INPUT', 'SELECT', 'TEXTAREA'],
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(
new Hotkey('shift+ctrl+f', (event: KeyboardEvent) => {
event.preventDefault();
@ -562,7 +543,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
private commitSaveWidget() {
const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : 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) => {
this.saveWidgetPending = false;
if (!this.widgetTypeDetails?.id) {
@ -594,11 +575,11 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
config.title = this.widget.widgetName;
this.widget.defaultConfig = JSON.stringify(config);
this.isDirty = false;
this.widgetService.saveWidgetTypeDetails(this.widget, undefined, saveWidgetAsData.bundleAlias, undefined).subscribe(
this.widgetService.saveWidgetTypeDetails(this.widget, undefined, undefined).subscribe(
{
next: (widgetTypeDetails) => {
this.saveWidgetAsPending = false;
this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeDetails.id.id}`);
this.router.navigate(['..', widgetTypeDetails.id.id], {relativeTo: this.route});
},
error: () => {
this.saveWidgetAsPending = false;
@ -655,34 +636,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
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 {
return !this.isDirty
|| !this.iframeWidgetEditModeInited
@ -704,15 +657,6 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
|| this.saveWidgetAsPending;
}
moveDisabled(): boolean {
return this.isReadOnly
|| !this.widgetTypeDetails?.id
|| !this.iframeWidgetEditModeInited
|| this.saveWidgetPending
|| this.saveWidgetAsPending
|| this.moveWidgetPending;
}
beautifyCss(): void {
beautifyCss(this.widget.templateCss, {indent_size: 4}).subscribe(
(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 { Authority } from '@shared/models/authority.enum';
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 { Observable } from 'rxjs';
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 { map } from 'rxjs/operators';
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 { WidgetsData } from '@home/models/dashboard-component.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { RouterTabsComponent } from '@home/components/router-tabs.component';
import { WidgetTypesTableConfigResolver } from '@home/pages/widget/widget-types-table-config.resolver';
import { WidgetsBundleWidgetsComponent } from '@home/pages/widget/widgets-bundle-widgets.component';
export interface WidgetEditorData {
widgetTypeDetails: WidgetTypeDetails;
@ -45,14 +45,24 @@ export class WidgetsBundleResolver implements Resolve<WidgetsBundle> {
}
resolve(route: ActivatedRouteSnapshot): Observable<WidgetsBundle> {
let widgetsBundleId = route.params.widgetsBundleId;
if (!widgetsBundleId) {
widgetsBundleId = route.parent.params.widgetsBundleId;
}
const widgetsBundleId = route.params.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()
export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
@ -60,15 +70,12 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
}
resolve(route: ActivatedRouteSnapshot): Observable<WidgetsData> {
const widgetsBundle: WidgetsBundle = route.parent.data.widgetsBundle;
const bundleAlias = widgetsBundle.alias;
const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
return this.widgetsService.loadBundleLibraryWidgets(bundleAlias,
isSystem).pipe(
const widgetsBundleId = route.params.widgetsBundleId;
return this.widgetsService.loadBundleLibraryWidgets(widgetsBundleId).pipe(
map((widgets) => ({ widgets })
));
}
}
}*/
@Injectable()
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);
export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetEditorComponent> =
@ -111,7 +118,49 @@ export const widgetEditorBreadcumbLabelFunction: BreadCrumbLabelFunction<WidgetE
component?.widget?.widgetName ?
(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',
data: {
@ -133,73 +182,91 @@ export const widgetsBundlesRoutes: Routes = [
}
},
{
path: ':widgetsBundleId/widgetTypes',
path: ':widgetsBundleId',
component: WidgetsBundleWidgetsComponent,
canDeactivate: [ConfirmOnExitGuard],
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'widgets-bundle.widgets-bundle-widgets',
breadcrumb: {
labelFunction: widgetTypesBreadcumbLabelFunction,
labelFunction: widgetsBundleWidgetsBreadcumbLabelFunction,
icon: 'now_widgets'
} as BreadCrumbConfig<any>
} as BreadCrumbConfig<any>,
hideTabs: true
},
resolve: {
widgetsBundle: WidgetsBundleResolver
},
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
}
}
]
widgetsBundle: WidgetsBundleResolver,
widgets: WidgetsBundleWidgetsResolver
}
}
]
},
];
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 = [
{
path: 'widgets-bundles',
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',
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',
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',
redirectTo: '/resources/widgets-bundles/:widgetsBundleId/widgetTypes/:widgetType',
redirectTo: '/resources/widgets-library/widget-types/: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)],
exports: [RouterModule],
providers: [
WidgetTypesTableConfigResolver,
WidgetsBundlesTableConfigResolver,
WidgetsBundleResolver,
WidgetsTypesDataResolver,
WidgetsBundleWidgetsResolver,
// WidgetsTypesDataResolver,
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 { WidgetLibraryRoutingModule } from '@modules/home/pages/widget/widget-library-routing.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 { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
import { SaveWidgetTypeAsDialogComponent } from './save-widget-type-as-dialog.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({
declarations: [
WidgetTypeComponent,
WidgetsBundleComponent,
WidgetLibraryComponent,
// WidgetLibraryComponent,
WidgetTypeAutocompleteComponent,
WidgetsBundleWidgetsComponent,
WidgetEditorComponent,
SelectWidgetTypeDialogComponent,
SaveWidgetTypeAsDialogComponent,
MoveWidgetTypeDialogComponent,
WidgetTypeTabsComponent,
WidgetsBundleTabsComponent
],
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;
};
this.config.entityAdded = widgetsBundle => {
this.openWidgetsBundle(null, 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',
dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards',
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',
widgetsConfigTimeseries: helpBaseUrl + '/docs/user-guide/ui/dashboards#timeseries',
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'
}
],
[
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,
{
@ -474,6 +487,12 @@ export const entityTypeResources = new Map<EntityType, EntityTypeResource<BaseDa
helpLinkId: 'dashboards'
}
],
[
EntityType.WIDGET_TYPE,
{
helpLinkId: 'widgetTypes'
}
],
[
EntityType.WIDGETS_BUNDLE,
{

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

@ -14,7 +14,7 @@
/// 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 { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/time/time.models';
@ -192,7 +192,6 @@ export interface WidgetControllerDescriptor {
export interface BaseWidgetType extends BaseData<WidgetTypeId> {
tenantId: TenantId;
bundleAlias: string;
fqn: string;
name: string;
deprecated: boolean;
@ -235,7 +234,7 @@ export interface WidgetTypeInfo extends BaseWidgetType {
widgetType: widgetType;
}
export interface WidgetTypeDetails extends WidgetType {
export interface WidgetTypeDetails extends WidgetType, ExportableEntity<WidgetTypeId> {
image: 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-telemetry-text": "No telemetry found",
"copy-key": "Copy key",
"copy-value": "Copy value",
"add-telemetry": "Add telemetry",
"copy-value": "Copy value",
"delete-timeseries": {
@ -2148,6 +2147,9 @@
"type-widgets-bundle": "Widgets bundle",
"type-widgets-bundles": "Widgets 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",
"selected-entities": "{ count, plural, =1 {1 entity} other {# entities} } selected",
"entity-name": "Entity name",
@ -4528,12 +4530,19 @@
"widget-bundle": "Widgets Bundle",
"all-bundles": "All bundles",
"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",
"editor": "Widget Editor",
"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:",
"remove": "Remove widget",
"delete": "Delete 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-text": "After the confirmation the widget and all related data will become unrecoverable.",
"timeseries": "Time series",
@ -4551,13 +4560,14 @@
"saveAs": "Save widget as",
"move": "Move widget",
"save-widget-type-as": "Save widget type as",
"save-widget-type-as-text": "Please enter new widget title and/or select target widgets bundle",
"move-widget-type": "Move widget type",
"move-widget-type-text": "Please select target widgets bundle",
"save-widget-type-as-text": "Please enter new widget title",
"toggle-fullscreen": "Toggle fullscreen",
"run": "Run widget",
"title": "Widget title",
"widget-title": "Widget title",
"title": "Title",
"title-required": "Widget title is required.",
"title-max-length": "Title should be less than 256",
"system": "System",
"type": "Widget type",
"resources": "Resources",
"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-text": "After the confirmation the widget type and all related data will become unrecoverable.",
"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",
"widget-type-load-failed-error": "Failed to load widget type!",
"widget-template-load-failed-error": "Failed to load widget template!",
"details": "Details",
"widget-type-details": "Widget type details",
"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",
"export": "Export widget",
"no-data": "No data to display on widget",
@ -4662,6 +4683,7 @@
"widgets-bundle": {
"current": "Current bundle",
"widgets-bundles": "Widgets Bundles",
"widgets-bundle-widgets": "Widgets Bundle Widgets",
"add": "Add Widgets Bundle",
"delete": "Delete widgets bundle",
"title": "Title",
@ -4684,6 +4706,7 @@
"system": "System",
"import": "Import 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}}",
"create-new-widgets-bundle": "Create new widgets bundle",
"widgets-bundle-file": "Widgets bundle file",
@ -4790,6 +4813,7 @@
"widget-type": {
"import": "Import widget type",
"export": "Export widget type",
"export-widget-types": "Export widget types",
"export-failed-error": "Unable to export widget type: {{error}}",
"create-new-widget-type": "Create new widget type",
"widget-type-file": "Widget type file",

Loading…
Cancel
Save