Browse Source

Merge branch 'feature/dashboard/widget-select-preview' of https://github.com/vvlladd28/thingsboard into vvlladd28-feature/dashboard/widget-select-preview

pull/4218/head
Igor Kulikov 6 years ago
parent
commit
23903588ac
  1. 5
      application/src/main/data/json/system/widget_bundles/alarm_widgets.json
  2. 13
      application/src/main/data/json/system/widget_bundles/analogue_gauges.json
  3. 19
      application/src/main/data/json/system/widget_bundles/cards.json
  4. 21
      application/src/main/data/json/system/widget_bundles/charts.json
  5. 21
      application/src/main/data/json/system/widget_bundles/control_widgets.json
  6. 5
      application/src/main/data/json/system/widget_bundles/date.json
  7. 29
      application/src/main/data/json/system/widget_bundles/digital_gauges.json
  8. 9
      application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json
  9. 9
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  10. 11
      application/src/main/data/json/system/widget_bundles/gpio_widgets.json
  11. 55
      application/src/main/data/json/system/widget_bundles/input_widgets.json
  12. 23
      application/src/main/data/json/system/widget_bundles/maps.json
  13. 7
      application/src/main/data/json/system/widget_bundles/navigation_widgets.json
  14. 23
      application/src/main/data/upgrade/3.2.1/schema_update.sql
  15. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  16. 11
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  17. 15
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java
  18. 22
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
  19. 3
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  20. 10
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java
  21. 10
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java
  22. 8
      dao/src/main/resources/sql/schema-entities-hsql.sql
  23. 8
      dao/src/main/resources/sql/schema-entities.sql
  24. 5
      ui-ngx/src/app/core/http/widget.service.ts
  25. 2
      ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts
  26. 2
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.ts
  27. 46
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  28. 94
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  29. 114
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html
  30. 58
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss
  31. 205
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  32. 23
      ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html
  33. 36
      ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.scss
  34. 48
      ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.ts
  35. 20
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  36. 2
      ui-ngx/src/app/modules/home/components/details-panel.component.scss
  37. 11
      ui-ngx/src/app/modules/home/components/details-panel.component.ts
  38. 7
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts
  39. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  40. 1
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  41. 4
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  42. 14
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  43. 29
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  44. 5
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  45. 10
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html
  46. 10
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts
  47. 1
      ui-ngx/src/app/shared/components/image-input.component.html
  48. 4
      ui-ngx/src/app/shared/components/image-input.component.scss
  49. 20
      ui-ngx/src/app/shared/components/image-input.component.ts
  50. 29
      ui-ngx/src/app/shared/components/widgets-bundle-search.component.html
  51. 55
      ui-ngx/src/app/shared/components/widgets-bundle-search.component.scss
  52. 74
      ui-ngx/src/app/shared/components/widgets-bundle-search.component.ts
  53. 1
      ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss
  54. 25
      ui-ngx/src/app/shared/models/widget.models.ts
  55. 1
      ui-ngx/src/app/shared/models/widgets-bundle.model.ts
  56. 56
      ui-ngx/src/app/shared/pipe/file-size.pipe.ts
  57. 1
      ui-ngx/src/app/shared/pipe/public-api.ts
  58. 11
      ui-ngx/src/app/shared/shared.module.ts
  59. 2
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  60. 2
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  61. 2
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  62. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  63. 2
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  64. 2
      ui-ngx/src/assets/locale/locale.constant-fa_IR.json
  65. 2
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  66. 2
      ui-ngx/src/assets/locale/locale.constant-it_IT.json
  67. 2
      ui-ngx/src/assets/locale/locale.constant-ja_JA.json
  68. 2
      ui-ngx/src/assets/locale/locale.constant-ka_GE.json
  69. 2
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json
  70. 2
      ui-ngx/src/assets/locale/locale.constant-lv_LV.json
  71. 2
      ui-ngx/src/assets/locale/locale.constant-pt_BR.json
  72. 2
      ui-ngx/src/assets/locale/locale.constant-ro_RO.json
  73. 2
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  74. 2
      ui-ngx/src/assets/locale/locale.constant-sl_SI.json
  75. 2
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  76. 2
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  77. 4
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  78. 2
      ui-ngx/src/assets/locale/locale.constant-zh_TW.json
  79. 1
      ui-ngx/src/assets/widget-preview-empty.svg
  80. 6
      ui-ngx/src/styles.scss

5
application/src/main/data/json/system/widget_bundles/alarm_widgets.json

File diff suppressed because one or more lines are too long

13
application/src/main/data/json/system/widget_bundles/analogue_gauges.json

File diff suppressed because one or more lines are too long

19
application/src/main/data/json/system/widget_bundles/cards.json

File diff suppressed because one or more lines are too long

21
application/src/main/data/json/system/widget_bundles/charts.json

File diff suppressed because one or more lines are too long

21
application/src/main/data/json/system/widget_bundles/control_widgets.json

File diff suppressed because one or more lines are too long

5
application/src/main/data/json/system/widget_bundles/date.json

@ -2,12 +2,15 @@
"widgetsBundle": { "widgetsBundle": {
"alias": "date", "alias": "date",
"title": "Date", "title": "Date",
"image": null "image": "data:image/png;base64,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",
"description": null
}, },
"widgetTypes": [ "widgetTypes": [
{ {
"alias": "date_range_navigator", "alias": "date_range_navigator",
"name": "Date-range-navigator", "name": "Date-range-navigator",
"image": "data:image/png;base64,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",
"description": null,
"descriptor": { "descriptor": {
"type": "static", "type": "static",
"sizeX": 5, "sizeX": 5,

29
application/src/main/data/json/system/widget_bundles/digital_gauges.json

File diff suppressed because one or more lines are too long

9
application/src/main/data/json/system/widget_bundles/entity_admin_widgets.json

@ -2,12 +2,15 @@
"widgetsBundle": { "widgetsBundle": {
"alias": "entity_admin_widgets", "alias": "entity_admin_widgets",
"title": "Entity admin widgets", "title": "Entity admin widgets",
"image": null "image": "data:image/png;base64,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",
"description": null
}, },
"widgetTypes": [ "widgetTypes": [
{ {
"alias": "device_admin_table", "alias": "device_admin_table",
"name": "Device admin table", "name": "Device admin table",
"image": "data:image/png;base64,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",
"description": null,
"descriptor": { "descriptor": {
"type": "latest", "type": "latest",
"sizeX": 7.5, "sizeX": 7.5,
@ -24,6 +27,8 @@
{ {
"alias": "asset_admin_table", "alias": "asset_admin_table",
"name": "Asset admin table", "name": "Asset admin table",
"image": "data:image/png;base64,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",
"description": null,
"descriptor": { "descriptor": {
"type": "latest", "type": "latest",
"sizeX": 7.5, "sizeX": 7.5,
@ -38,4 +43,4 @@
} }
} }
] ]
} }

9
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

File diff suppressed because one or more lines are too long

11
application/src/main/data/json/system/widget_bundles/gpio_widgets.json

File diff suppressed because one or more lines are too long

55
application/src/main/data/json/system/widget_bundles/input_widgets.json

File diff suppressed because one or more lines are too long

23
application/src/main/data/json/system/widget_bundles/maps.json

File diff suppressed because one or more lines are too long

7
application/src/main/data/json/system/widget_bundles/navigation_widgets.json

File diff suppressed because one or more lines are too long

23
application/src/main/data/upgrade/3.2.1/schema_update.sql

@ -0,0 +1,23 @@
--
-- Copyright © 2016-2021 The Thingsboard Authors
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
ALTER TABLE widget_type
ADD COLUMN IF NOT EXISTS image varchar (1000000),
ADD COLUMN IF NOT EXISTS description varchar (255);
ALTER TABLE widgets_bundle
ADD COLUMN IF NOT EXISTS image varchar (1000000),
ADD COLUMN IF NOT EXISTS description varchar (255);

2
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -190,7 +190,7 @@ public class ThingsboardInstallService {
if (databaseTsUpgradeService != null) { if (databaseTsUpgradeService != null) {
databaseTsUpgradeService.upgradeDatabase("3.2.1"); databaseTsUpgradeService.upgradeDatabase("3.2.1");
} }
databaseEntitiesUpgradeService.upgradeDatabase("3.2.1");
log.info("Updating system data..."); log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets(); systemDataLoaderService.updateSystemWidgets();
break; break;

11
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -434,6 +434,17 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
log.info("Schema updated."); log.info("Schema updated.");
} }
break; break;
case "3.2.1":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.1", SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, conn);
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;");
log.info("Schema updated.");
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
}
break;
default: default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
} }

15
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java

@ -31,6 +31,8 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
private String bundleAlias; private String bundleAlias;
private String alias; private String alias;
private String name; private String name;
private String image;
private String description;
private transient JsonNode descriptor; private transient JsonNode descriptor;
public WidgetType() { public WidgetType() {
@ -47,6 +49,8 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
this.bundleAlias = widgetType.getBundleAlias(); this.bundleAlias = widgetType.getBundleAlias();
this.alias = widgetType.getAlias(); this.alias = widgetType.getAlias();
this.name = widgetType.getName(); this.name = widgetType.getName();
this.image = widgetType.getImage();
this.description = widgetType.getDescription();
this.descriptor = widgetType.getDescriptor(); this.descriptor = widgetType.getDescriptor();
} }
@ -82,6 +86,14 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
this.name = name; this.name = name;
} }
public String getImage() { return image; }
public void setImage(String image) { this.image = image; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public JsonNode getDescriptor() { public JsonNode getDescriptor() {
return descriptor; return descriptor;
} }
@ -97,9 +109,10 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
sb.append(", bundleAlias='").append(bundleAlias).append('\''); sb.append(", bundleAlias='").append(bundleAlias).append('\'');
sb.append(", alias='").append(alias).append('\''); sb.append(", alias='").append(alias).append('\'');
sb.append(", name='").append(name).append('\''); sb.append(", name='").append(name).append('\'');
sb.append(", image='").append(image).append('\'');
sb.append(", description='").append(description).append('\'');
sb.append(", descriptor=").append(descriptor); sb.append(", descriptor=").append(descriptor);
sb.append('}'); sb.append('}');
return sb.toString(); return sb.toString();
} }
} }

22
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java

@ -29,7 +29,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
private TenantId tenantId; private TenantId tenantId;
private String alias; private String alias;
private String title; private String title;
private byte[] image; private String image;
private String description;
public WidgetsBundle() { public WidgetsBundle() {
super(); super();
@ -45,6 +46,7 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
this.alias = widgetsBundle.getAlias(); this.alias = widgetsBundle.getAlias();
this.title = widgetsBundle.getTitle(); this.title = widgetsBundle.getTitle();
this.image = widgetsBundle.getImage(); this.image = widgetsBundle.getImage();
this.description = widgetsBundle.getDescription();
} }
public TenantId getTenantId() { public TenantId getTenantId() {
@ -71,14 +73,18 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
this.title = title; this.title = title;
} }
public byte[] getImage() { public String getImage() {
return image; return image;
} }
public void setImage(byte[] image) { public void setImage(String image) {
this.image = image; this.image = image;
} }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
@Override @Override
public String getSearchText() { public String getSearchText() {
return getTitle(); return getTitle();
@ -90,7 +96,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0); result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
result = 31 * result + (alias != null ? alias.hashCode() : 0); result = 31 * result + (alias != null ? alias.hashCode() : 0);
result = 31 * result + (title != null ? title.hashCode() : 0); result = 31 * result + (title != null ? title.hashCode() : 0);
result = 31 * result + Arrays.hashCode(image); result = 31 * result + (image != null ? image.hashCode() : 0);
result = 31 * result + (description != null ? description.hashCode() : 0);
return result; return result;
} }
@ -105,7 +112,9 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false; if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false;
if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false; if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false;
if (title != null ? !title.equals(that.title) : that.title != null) return false; if (title != null ? !title.equals(that.title) : that.title != null) return false;
return Arrays.equals(image, that.image); if (image != null ? !image.equals(that.image) : that.image != null) return false;
if (description != null ? !description.equals(that.image) : that.description != null) return false;
return true;
} }
@Override @Override
@ -114,7 +123,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
sb.append("tenantId=").append(tenantId); sb.append("tenantId=").append(tenantId);
sb.append(", alias='").append(alias).append('\''); sb.append(", alias='").append(alias).append('\'');
sb.append(", title='").append(title).append('\''); sb.append(", title='").append(title).append('\'');
sb.append(", image=").append(Arrays.toString(image)); sb.append(", image='").append(image).append('\'');
sb.append(", description='").append(description).append('\'');
sb.append('}'); sb.append('}');
return sb.toString(); return sb.toString();
} }

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

@ -304,6 +304,7 @@ public class ModelConstants {
public static final String WIDGETS_BUNDLE_ALIAS_PROPERTY = ALIAS_PROPERTY; public static final String WIDGETS_BUNDLE_ALIAS_PROPERTY = ALIAS_PROPERTY;
public static final String WIDGETS_BUNDLE_TITLE_PROPERTY = TITLE_PROPERTY; public static final String WIDGETS_BUNDLE_TITLE_PROPERTY = TITLE_PROPERTY;
public static final String WIDGETS_BUNDLE_IMAGE_PROPERTY = "image"; public static final String WIDGETS_BUNDLE_IMAGE_PROPERTY = "image";
public static final String WIDGETS_BUNDLE_DESCRIPTION = "description";
public static final String WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_search_text"; public static final String WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_search_text";
public static final String WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_alias"; public static final String WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_alias";
@ -316,6 +317,8 @@ public class ModelConstants {
public static final String WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY = "bundle_alias"; public static final String WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY = "bundle_alias";
public static final String WIDGET_TYPE_ALIAS_PROPERTY = ALIAS_PROPERTY; public static final String WIDGET_TYPE_ALIAS_PROPERTY = ALIAS_PROPERTY;
public static final String WIDGET_TYPE_NAME_PROPERTY = "name"; public static final String WIDGET_TYPE_NAME_PROPERTY = "name";
public static final String WIDGET_TYPE_IMAGE_PROPERTY = "image";
public static final String WIDGET_TYPE_DESCRIPTION_PROPERTY = "description";
public static final String WIDGET_TYPE_DESCRIPTOR_PROPERTY = "descriptor"; public static final String WIDGET_TYPE_DESCRIPTOR_PROPERTY = "descriptor";
public static final String WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME = "widget_type_by_tenant_and_aliases"; public static final String WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME = "widget_type_by_tenant_and_aliases";

10
dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java

@ -52,6 +52,12 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement
@Column(name = ModelConstants.WIDGET_TYPE_NAME_PROPERTY) @Column(name = ModelConstants.WIDGET_TYPE_NAME_PROPERTY)
private String name; private String name;
@Column(name = ModelConstants.WIDGET_TYPE_IMAGE_PROPERTY)
private String image;
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTION_PROPERTY)
private String description;
@Type(type="json") @Type(type="json")
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY) @Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY)
private JsonNode descriptor; private JsonNode descriptor;
@ -71,6 +77,8 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement
this.bundleAlias = widgetType.getBundleAlias(); this.bundleAlias = widgetType.getBundleAlias();
this.alias = widgetType.getAlias(); this.alias = widgetType.getAlias();
this.name = widgetType.getName(); this.name = widgetType.getName();
this.image = widgetType.getImage();
this.description = widgetType.getDescription();
this.descriptor = widgetType.getDescriptor(); this.descriptor = widgetType.getDescriptor();
} }
@ -84,6 +92,8 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement
widgetType.setBundleAlias(bundleAlias); widgetType.setBundleAlias(bundleAlias);
widgetType.setAlias(alias); widgetType.setAlias(alias);
widgetType.setName(name); widgetType.setName(name);
widgetType.setImage(image);
widgetType.setDescription(description);
widgetType.setDescriptor(descriptor); widgetType.setDescriptor(descriptor);
return widgetType; return widgetType;
} }

10
dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java

@ -48,6 +48,12 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl
@Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
private String searchText; private String searchText;
@Column(name = ModelConstants.WIDGETS_BUNDLE_IMAGE_PROPERTY)
private String image;
@Column(name = ModelConstants.WIDGETS_BUNDLE_DESCRIPTION)
private String description;
public WidgetsBundleEntity() { public WidgetsBundleEntity() {
super(); super();
} }
@ -62,6 +68,8 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl
} }
this.alias = widgetsBundle.getAlias(); this.alias = widgetsBundle.getAlias();
this.title = widgetsBundle.getTitle(); this.title = widgetsBundle.getTitle();
this.image = widgetsBundle.getImage();
this.description = widgetsBundle.getDescription();
} }
@Override @Override
@ -83,6 +91,8 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl
} }
widgetsBundle.setAlias(alias); widgetsBundle.setAlias(alias);
widgetsBundle.setTitle(title); widgetsBundle.setTitle(title);
widgetsBundle.setImage(image);
widgetsBundle.setDescription(description);
return widgetsBundle; return widgetsBundle;
} }
} }

8
dao/src/main/resources/sql/schema-entities-hsql.sql

@ -290,7 +290,9 @@ CREATE TABLE IF NOT EXISTS widget_type (
bundle_alias varchar(255), bundle_alias varchar(255),
descriptor varchar(1000000), descriptor varchar(1000000),
name varchar(255), name varchar(255),
tenant_id uuid tenant_id uuid,
image varchar(1000000),
description varchar(255)
); );
CREATE TABLE IF NOT EXISTS widgets_bundle ( CREATE TABLE IF NOT EXISTS widgets_bundle (
@ -299,7 +301,9 @@ CREATE TABLE IF NOT EXISTS widgets_bundle (
alias varchar(255), alias varchar(255),
search_text varchar(255), search_text varchar(255),
tenant_id uuid, tenant_id uuid,
title varchar(255) title varchar(255),
image varchar(1000000),
description varchar(255)
); );
CREATE TABLE IF NOT EXISTS entity_view ( CREATE TABLE IF NOT EXISTS entity_view (

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

@ -315,7 +315,9 @@ CREATE TABLE IF NOT EXISTS widget_type (
bundle_alias varchar(255), bundle_alias varchar(255),
descriptor varchar(1000000), descriptor varchar(1000000),
name varchar(255), name varchar(255),
tenant_id uuid tenant_id uuid,
image varchar(1000000),
description varchar(255)
); );
CREATE TABLE IF NOT EXISTS widgets_bundle ( CREATE TABLE IF NOT EXISTS widgets_bundle (
@ -324,7 +326,9 @@ CREATE TABLE IF NOT EXISTS widgets_bundle (
alias varchar(255), alias varchar(255),
search_text varchar(255), search_text varchar(255),
tenant_id uuid, tenant_id uuid,
title varchar(255) title varchar(255),
image varchar(1000000),
description varchar(255)
); );
CREATE TABLE IF NOT EXISTS entity_view ( CREATE TABLE IF NOT EXISTS entity_view (

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

@ -144,6 +144,8 @@ export class WidgetService {
typeAlias: widgetTypeInfo.alias, typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.type, type: widgetTypeInfo.type,
title: widgetTypeInfo.widgetName, title: widgetTypeInfo.widgetName,
image: widgetTypeInfo.image,
description: widgetTypeInfo.description,
sizeX, sizeX,
sizeY, sizeY,
row: top, row: top,
@ -179,8 +181,9 @@ export class WidgetService {
public saveWidgetType(widgetInfo: WidgetInfo, public saveWidgetType(widgetInfo: WidgetInfo,
id: WidgetTypeId, id: WidgetTypeId,
bundleAlias: string, bundleAlias: string,
createdTime: number,
config?: RequestConfig): Observable<WidgetType> { config?: RequestConfig): Observable<WidgetType> {
const widgetTypeInstance = toWidgetType(widgetInfo, id, undefined, bundleAlias); const widgetTypeInstance = toWidgetType(widgetInfo, id, undefined, bundleAlias, createdTime);
return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance, return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance,
defaultHttpOptionsFromConfig(config)).pipe( defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => { tap((savedWidgetType) => {

2
ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts

@ -44,6 +44,8 @@ export class WidgetEditorDashboardResolver implements Resolve<Dashboard> {
typeAlias: 'customWidget', typeAlias: 'customWidget',
type: editWidgetInfo.type, type: editWidgetInfo.type,
title: 'My widget', title: 'My widget',
image: null,
description: null,
sizeX: editWidgetInfo.sizeX * 2, sizeX: editWidgetInfo.sizeX * 2,
sizeY: editWidgetInfo.sizeY * 2, sizeY: editWidgetInfo.sizeY * 2,
row: 2, row: 2,

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

@ -470,6 +470,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
typeAlias: widgetInfo.alias, typeAlias: widgetInfo.alias,
type: widgetInfo.type, type: widgetInfo.type,
title: widgetInfo.widgetName, title: widgetInfo.widgetName,
image: widgetInfo.image,
description: widgetInfo.description,
sizeX, sizeX,
sizeY, sizeY,
row: 0, row: 0,

46
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -242,26 +242,44 @@
</tb-edit-widget> </tb-edit-widget>
</tb-details-panel> </tb-details-panel>
<tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex <tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex
headerTitle="{{'dashboard.select-widget-title' | translate}}" headerTitle="{{
headerHeightPx="120" (!widgetsBundle?.title ? 'widget.select-widgets-bundle' : 'dashboard.select-widget-value') | translate: widgetsBundle
}}"
headerHeightPx="64"
[isShowSearch]="true"
[isReadOnly]="true" [isReadOnly]="true"
[isEdit]="false" [isEdit]="false"
(closeDetails)="onAddWidgetClosed()"> backgroundColor="#cfd8dc"
<div class="header-pane" *ngIf="isAddingWidget"> (closeDetails)="onAddWidgetClosed()"
<div fxLayout="row"> (closeSearch)="onCloseSearchBundle()">
<span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span> <div class="prefix-title-buttons" [fxHide]="!widgetsBundle?.title" style="height: 28px; margin-right: 12px">
<tb-widgets-bundle-select fxFlexOffset="5" <button class="tb-mat-28" mat-icon-button type="button" (click)="widgetBundleSelected(null)">
fxFlex <mat-icon>arrow_back</mat-icon>
required </button>
[selectFirstBundle]="false" </div>
[ngModel]="widgetsBundle" <div class="search-pane" *ngIf="isAddingWidget" fxLayout="row">
(ngModelChange)="widgetsBundle = $event"> <tb-widgets-bundle-search fxFlex
</tb-widgets-bundle-select> [(ngModel)]="searchBundle"
</div> placeholder="{{ (!widgetsBundle?.title ? 'widgets-bundle.search' : 'widget.search') | translate }}"
(ngModelChange)="searchBundle = $event">
</tb-widgets-bundle-search>
</div>
<div class="details-buttons" *ngIf="isAddingWidget">
<button mat-button mat-icon-button type="button"
*ngIf="widgetTypes.length > 1"
(click)="editWidgetsTypesToDisplay($event)"
matTooltip="{{ 'widget.filter' | translate }}"
matTooltipPosition="above">
<mat-icon>filter_list</mat-icon>
</button>
</div> </div>
<tb-dashboard-widget-select *ngIf="isAddingWidget" <tb-dashboard-widget-select *ngIf="isAddingWidget"
[aliasController]="dashboardCtx.aliasController" [aliasController]="dashboardCtx.aliasController"
[widgetsBundle]="widgetsBundle" [widgetsBundle]="widgetsBundle"
[searchBundle]="searchBundle"
[filterWidgetTypes]="filterWidgetTypes"
(widgetsTypes)="updateWidgetsTypes($event)"
(widgetsBundleSelected)="widgetBundleSelected($event)"
(widgetSelected)="addWidgetFromType($event)"> (widgetSelected)="addWidgetFromType($event)">
</tb-dashboard-widget-select> </tb-dashboard-widget-select>
</tb-details-panel> </tb-details-panel>

94
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -18,11 +18,14 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
Inject, Inject,
Injector,
Input, Input,
NgZone, NgZone,
OnDestroy, OnDestroy,
OnInit, OnInit,
StaticProvider,
ViewChild, ViewChild,
ViewContainerRef,
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
@ -58,7 +61,14 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
import { Widget, WidgetConfig, WidgetPosition, widgetTypesData } from '@app/shared/models/widget.models'; import {
Widget,
WidgetConfig,
WidgetInfo,
WidgetPosition,
widgetType,
widgetTypesData
} from '@shared/models/widget.models';
import { environment as env } from '@env/environment'; import { environment as env } from '@env/environment';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
@ -80,7 +90,10 @@ import {
import { EntityAliases } from '@app/shared/models/alias.models'; import { EntityAliases } from '@app/shared/models/alias.models';
import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component'; import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { AddWidgetDialogComponent, AddWidgetDialogData } from '@home/components/dashboard-page/add-widget-dialog.component'; import {
AddWidgetDialogComponent,
AddWidgetDialogData
} from '@home/components/dashboard-page/add-widget-dialog.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
ManageDashboardLayoutsDialogComponent, ManageDashboardLayoutsDialogComponent,
@ -99,6 +112,14 @@ import { ImportExportService } from '@home/components/import-export/import-expor
import { AuthState } from '@app/core/auth/auth.models'; import { AuthState } from '@app/core/auth/auth.models';
import { FiltersDialogComponent, FiltersDialogData } from '@home/components/filter/filters-dialog.component'; import { FiltersDialogComponent, FiltersDialogData } from '@home/components/filter/filters-dialog.component';
import { Filters } from '@shared/models/query/query.models'; import { Filters } from '@shared/models/query/query.models';
import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import {
DISPLAY_WIDGET_TYPES_PANEL_DATA,
DisplayWidgetTypesPanelComponent,
DisplayWidgetTypesPanelData,
WidgetTypes
} from '@home/components/dashboard-page/widget-types-panel.component';
// @dynamic // @dynamic
@Component({ @Component({
@ -146,6 +167,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
isAddingWidget = false; isAddingWidget = false;
isAddingWidgetClosed = true; isAddingWidgetClosed = true;
widgetsBundle: WidgetsBundle = null; widgetsBundle: WidgetsBundle = null;
searchBundle = '';
widgetTypes: WidgetTypes[] = [];
filterWidgetTypes: widgetType[] = null;
isToolbarOpened = false; isToolbarOpened = false;
isToolbarOpenedAnimate = false; isToolbarOpenedAnimate = false;
@ -260,6 +284,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private dialog: MatDialog, private dialog: MatDialog,
private translate: TranslateService, private translate: TranslateService,
private ngZone: NgZone, private ngZone: NgZone,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
super(store); super(store);
@ -856,8 +882,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
} }
addWidgetFromType(widget: Widget) { addWidgetFromType(widget: WidgetInfo) {
this.onAddWidgetClosed(); this.onAddWidgetClosed();
this.widgetTypes = [];
this.searchBundle = '';
this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe( this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe(
(widgetTypeInfo) => { (widgetTypeInfo) => {
const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig); const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig);
@ -869,6 +897,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
typeAlias: widgetTypeInfo.alias, typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.type, type: widgetTypeInfo.type,
title: 'New widget', title: 'New widget',
image: null,
description: null,
sizeX: widgetTypeInfo.sizeX, sizeX: widgetTypeInfo.sizeX,
sizeY: widgetTypeInfo.sizeY, sizeY: widgetTypeInfo.sizeY,
config, config,
@ -1115,4 +1145,62 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
return widgetContextActions; return widgetContextActions;
} }
widgetBundleSelected(bundle: WidgetsBundle){
this.widgetsBundle = bundle;
this.widgetTypes = [];
this.searchBundle = '';
}
updateWidgetsTypes(types: Set<widgetType>) {
this.widgetTypes = Array.from(types.values()).map(type => {
return {type, display: true};
});
}
editWidgetsTypesToDisplay($event: Event) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const providers: StaticProvider[] = [
{
provide: DISPLAY_WIDGET_TYPES_PANEL_DATA,
useValue: {
types: this.widgetTypes,
typesUpdated: (newTypes) => {
this.filterWidgetTypes = newTypes.filter(type => type.display).map(type => type.type);
}
} as DisplayWidgetTypesPanelData
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayWidgetTypesPanelComponent, this.viewContainerRef, injector));
this.cd.detectChanges();
}
onCloseSearchBundle() {
this.searchBundle = '';
}
} }

114
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html

@ -15,75 +15,47 @@
limitations under the License. limitations under the License.
--> -->
<div> <div class="widget-select">
<mat-tab-group *ngIf="hasWidgetTypes()" class="tb-absolute-fill" fxFlex> <div *ngIf="widgetsBundle; else bundles">
<mat-tab *ngIf="timeseriesWidgetTypes.length" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}"> <div *ngIf="(widgets$ | async)?.length; else emptyBundle" fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
<tb-dashboard [aliasController]="aliasController" <div *ngFor="let widget of widgets$ | async" class="mat-card-container">
[widgets]="timeseriesWidgetTypes" <mat-card fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="onWidgetClicked($event, widget)">
[widgetLayouts]="{}" <div class="preview-container" fxFlex="45">
[isEdit]="false" <img class="preview" [src]="getPreviewImage(widget.image)" alt="{{ widget.title }}">
[isMobile]="true" </div>
[disableWidgetInteraction]="true" <div fxFlex fxLayout="column">
[isEditActionEnabled]="false" <mat-card-title>{{widget.title}}</mat-card-title>
[isExportActionEnabled]="false" <mat-card-subtitle>{{ 'widget.' + widget.type | translate }}</mat-card-subtitle>
[isRemoveActionEnabled]="false" <mat-card-content *ngIf="widget.description">
[callbacks]="callbacks"></tb-dashboard> {{ widget.description }}
</mat-tab> </mat-card-content>
<mat-tab *ngIf="latestWidgetTypes.length" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}"> </div>
<tb-dashboard [aliasController]="aliasController" </mat-card>
[widgets]="latestWidgetTypes" </div>
[widgetLayouts]="{}" </div>
[isEdit]="false" <ng-template #emptyBundle>
[isMobile]="true" <span translate
[disableWidgetInteraction]="true" style="display: flex;"
[isEditActionEnabled]="false" fxLayoutAlign="center center"
[isExportActionEnabled]="false" class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
[isRemoveActionEnabled]="false" </ng-template>
[callbacks]="callbacks"></tb-dashboard> </div>
</mat-tab> <ng-template #bundles>
<mat-tab *ngIf="rpcWidgetTypes.length" style="height: 100%;" label="{{ 'widget.rpc' | translate }}"> <div fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
<tb-dashboard [aliasController]="aliasController" <div *ngFor="let widgetsBundle of widgetsBundles$ | async" class="mat-card-container">
[widgets]="rpcWidgetTypes" <mat-card fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="selectBundle($event, widgetsBundle)">
[widgetLayouts]="{}" <div class="preview-container" fxFlex="45">
[isEdit]="false" <img class="preview" [src]=getPreviewImage(widgetsBundle.image) alt="{{ widgetsBundle.title }}">
[isMobile]="true" </div>
[disableWidgetInteraction]="true" <div fxFlex fxLayout="column">
[isEditActionEnabled]="false" <mat-card-title>{{ widgetsBundle.title }}</mat-card-title>
[isExportActionEnabled]="false" <mat-card-subtitle *ngIf="isSystem(widgetsBundle)" translate>widgets-bundle.system</mat-card-subtitle>
[isRemoveActionEnabled]="false" <mat-card-content *ngIf="widgetsBundle.description">
[callbacks]="callbacks"></tb-dashboard> {{ widgetsBundle.description }}
</mat-tab> </mat-card-content>
<mat-tab *ngIf="alarmWidgetTypes.length" style="height: 100%;" label="{{ 'widget.alarm' | translate }}"> </div>
<tb-dashboard [aliasController]="aliasController" </mat-card>
[widgets]="alarmWidgetTypes" </div>
[widgetLayouts]="{}" </div>
[isEdit]="false" </ng-template>
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
<mat-tab *ngIf="staticWidgetTypes.length" style="height: 100%;" label="{{ 'widget.static' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="staticWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
</mat-tab-group>
<span translate *ngIf="widgetsBundle && !hasWidgetTypes()"
style="display: flex;"
fxLayoutAlign="center center"
class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
<span translate *ngIf="!widgetsBundle"
style="display: flex;"
fxLayoutAlign="center center"
class="mat-headline tb-absolute-fill">widget.select-widgets-bundle</span>
</div> </div>

58
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss

@ -13,10 +13,60 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host ::ng-deep { @import '../../../../../scss/constants';
.mat-tab-group {
.mat-tab-body-wrapper { :host{
height: 100%; min-width: 100%;
min-height: 100%;
position: absolute;
background-color: #cfd8dc;
.widget-select {
padding: 12px 0 12px 12px;
background-color: #cfd8dc;
.mat-card-container {
flex: 0 0 100%;
max-width: 100%;
.mat-card {
cursor: pointer;
.preview-container {
text-align: center;
margin: auto 0;
}
.preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.mat-card-title {
font-size: 20px;
}
.mat-card-content {
font-size: 14px;
line-height: 18px;
color: #666;
}
}
}
@media #{$mat-gt-xs} {
.mat-card-container {
flex: 0 0 50%;
max-width: 50%;
}
}
@media screen and (min-width: 2000px) {
.mat-card-container {
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
} }
} }
} }

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

@ -14,120 +14,165 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { NULL_UUID } from '@shared/models/id/has-uuid'; import { NULL_UUID } from '@shared/models/id/has-uuid';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { Widget, widgetType } from '@shared/models/widget.models'; import { WidgetInfo, widgetType } from '@shared/models/widget.models';
import { toWidgetInfo } from '@home/models/widget-component.models'; import { toWidgetInfo } from '@home/models/widget-component.models';
import { DashboardCallbacks } from '../../models/dashboard-component.models'; import { distinctUntilChanged, map, publishReplay, refCount, switchMap, tap } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { isDefinedAndNotNull } from '@core/utils';
@Component({ @Component({
selector: 'tb-dashboard-widget-select', selector: 'tb-dashboard-widget-select',
templateUrl: './dashboard-widget-select.component.html', templateUrl: './dashboard-widget-select.component.html',
styleUrls: ['./dashboard-widget-select.component.scss'] styleUrls: ['./dashboard-widget-select.component.scss']
}) })
export class DashboardWidgetSelectComponent implements OnInit, OnChanges { export class DashboardWidgetSelectComponent implements OnInit {
private search$ = new BehaviorSubject<string>('');
private filterWidgetTypes$ = new BehaviorSubject<Array<widgetType>>(null);
private widgetsInfo: Observable<Array<WidgetInfo>>;
private widgetsBundleValue: WidgetsBundle;
private widgetsType = new Set<widgetType>();
widgets$: Observable<Array<WidgetInfo>>;
widgetsBundles$: Observable<Array<WidgetsBundle>>;
@Input() @Input()
widgetsBundle: WidgetsBundle; set widgetsBundle(widgetBundle: WidgetsBundle) {
this.widgetsInfo = null;
this.widgetsType.clear();
this.widgetsTypes.emit(this.widgetsType);
this.filterWidgetTypes$.next(null);
this.widgetsBundleValue = widgetBundle;
}
get widgetsBundle(): WidgetsBundle {
return this.widgetsBundleValue;
}
@Input() @Input()
aliasController: IAliasController; aliasController: IAliasController;
@Input()
set searchBundle(search: string) {
this.search$.next(search);
}
@Input()
set filterWidgetTypes(widgetTypes: Array<widgetType>) {
this.filterWidgetTypes$.next(widgetTypes);
}
@Output() @Output()
widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>(); widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>();
timeseriesWidgetTypes: Array<Widget> = []; @Output()
latestWidgetTypes: Array<Widget> = []; widgetsBundleSelected: EventEmitter<WidgetsBundle> = new EventEmitter<WidgetsBundle>();
rpcWidgetTypes: Array<Widget> = [];
alarmWidgetTypes: Array<Widget> = [];
staticWidgetTypes: Array<Widget> = [];
callbacks: DashboardCallbacks = { @Output()
onWidgetClicked: this.onWidgetClicked.bind(this) widgetsTypes: EventEmitter<Set<widgetType>> = new EventEmitter<Set<widgetType>>();
};
constructor(private widgetsService: WidgetService) { constructor(private widgetsService: WidgetService,
private sanitizer: DomSanitizer) {
} }
ngOnInit(): void { ngOnInit(): void {
this.widgetsBundles$ = this.search$.asObservable().pipe(
distinctUntilChanged(),
switchMap(search => this.fetchWidgetBundle(search))
);
this.widgets$ = combineLatest([this.search$.asObservable(), this.filterWidgetTypes$.asObservable()]).pipe(
distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)),
switchMap(search => this.fetchWidget(...search))
);
} }
ngOnChanges(changes: SimpleChanges): void { private getWidgets(): Observable<Array<WidgetInfo>> {
for (const propName of Object.keys(changes)) { if (!this.widgetsInfo) {
const change = changes[propName]; if (this.widgetsBundle !== null) {
if (change.currentValue !== change.previousValue && change.currentValue) { const bundleAlias = this.widgetsBundle.alias;
if (propName === 'widgetsBundle') { const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
this.loadLibrary(); this.widgetsInfo = this.widgetsService.getBundleWidgetTypes(bundleAlias, isSystem).pipe(
} map(widgets => widgets.sort((a, b) => b.createdTime - a.createdTime)),
map(widgets => widgets.map((type) => {
const widgetTypeInfo = toWidgetInfo(type);
this.widgetsType.add(widgetTypeInfo.type);
const widget: WidgetInfo = {
isSystemType: isSystem,
bundleAlias,
typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.type,
title: widgetTypeInfo.widgetName,
image: widgetTypeInfo.image,
description: widgetTypeInfo.description
};
return widget;
})),
tap(() => this.widgetsTypes.emit(this.widgetsType)),
publishReplay(1),
refCount()
);
} else {
this.widgetsInfo = of([]);
} }
} }
return this.widgetsInfo;
} }
private loadLibrary() { onWidgetClicked($event: Event, widget: WidgetInfo): void {
this.timeseriesWidgetTypes.length = 0; this.widgetSelected.emit(widget);
this.latestWidgetTypes.length = 0;
this.rpcWidgetTypes.length = 0;
this.alarmWidgetTypes.length = 0;
this.staticWidgetTypes.length = 0;
const bundleAlias = this.widgetsBundle.alias;
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
this.widgetsService.getBundleWidgetTypes(bundleAlias,
isSystem).subscribe(
(types) => {
types = types.sort((a, b) => b.createdTime - a.createdTime);
let top = 0;
types.forEach((type) => {
const widgetTypeInfo = toWidgetInfo(type);
const widget: Widget = {
typeId: type.id,
isSystemType: isSystem,
bundleAlias,
typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.type,
title: widgetTypeInfo.widgetName,
sizeX: widgetTypeInfo.sizeX,
sizeY: widgetTypeInfo.sizeY,
row: top,
col: 0,
config: JSON.parse(widgetTypeInfo.defaultConfig)
};
widget.config.title = widgetTypeInfo.widgetName;
switch (widgetTypeInfo.type) {
case widgetType.timeseries:
this.timeseriesWidgetTypes.push(widget);
break;
case widgetType.latest:
this.latestWidgetTypes.push(widget);
break;
case widgetType.rpc:
this.rpcWidgetTypes.push(widget);
break;
case widgetType.alarm:
this.alarmWidgetTypes.push(widget);
break;
case widgetType.static:
this.staticWidgetTypes.push(widget);
break;
}
top += widget.sizeY;
});
}
);
} }
hasWidgetTypes() { isSystem(item: WidgetsBundle): boolean {
return this.timeseriesWidgetTypes.length > 0 || return item && item.tenantId.id === NULL_UUID;
this.latestWidgetTypes.length > 0 ||
this.rpcWidgetTypes.length > 0 ||
this.alarmWidgetTypes.length > 0 ||
this.staticWidgetTypes.length > 0;
} }
private onWidgetClicked($event: Event, widget: Widget, index: number): void { selectBundle($event: Event, bundle: WidgetsBundle) {
this.widgetSelected.emit(widget); $event.preventDefault();
this.widgetsBundle = bundle;
this.search$.next('');
this.widgetsBundleSelected.emit(bundle);
} }
getPreviewImage(imageUrl: string | null): SafeUrl | string {
if (isDefinedAndNotNull(imageUrl)) {
return this.sanitizer.bypassSecurityTrustUrl(imageUrl);
}
return '/assets/widget-preview-empty.svg';
}
private getWidgetsBundle(): Observable<Array<WidgetsBundle>> {
return this.widgetsService.getAllWidgetsBundles().pipe(
publishReplay(1),
refCount()
);
}
private fetchWidgetBundle(search: string): Observable<Array<WidgetsBundle>> {
return this.getWidgetsBundle().pipe(
map(bundles => search ? bundles.filter(
bundle => (
bundle.title?.toLowerCase().includes(search.toLowerCase()) ||
bundle.description?.toLowerCase().includes(search.toLowerCase())
)) : bundles
)
);
}
private fetchWidget(search: string, filter: widgetType[]): Observable<Array<WidgetInfo>> {
return this.getWidgets().pipe(
map(widgets => filter ? widgets.filter((widget) => filter.includes(widget.type)) : widgets),
map(widgets => search ? widgets.filter(
widget => (
widget.title?.toLowerCase().includes(search.toLowerCase()) ||
widget.description?.toLowerCase().includes(search.toLowerCase())
)) : widgets
)
);
}
} }

23
ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html

@ -0,0 +1,23 @@
<!--
Copyright © 2016-2021 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 fxLayout="column" class="mat-content mat-padding">
<mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="type.display" *ngFor="let type of types"
(ngModelChange)="update()">
{{ 'widget.' + type.type | translate }}
</mat-checkbox>
</div>

36
ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.scss

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2021 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.
*/
:host {
width: 100%;
height: 100%;
min-width: 200px;
overflow: hidden;
background: #fff;
border-radius: 4px;
box-shadow:
0 7px 8px -4px rgba(0, 0, 0, .2),
0 13px 19px 2px rgba(0, 0, 0, .14),
0 5px 24px 4px rgba(0, 0, 0, .12);
.mat-content {
overflow: hidden;
background-color: #fff;
}
.mat-padding {
padding: 16px;
}
}

48
ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.ts

@ -0,0 +1,48 @@
///
/// Copyright © 2016-2021 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, InjectionToken } from '@angular/core';
import { widgetType } from '@shared/models/widget.models';
export const DISPLAY_WIDGET_TYPES_PANEL_DATA = new InjectionToken<any>('DisplayWidgetTypesPanelData');
export interface WidgetTypes {
type: widgetType;
display: boolean;
}
export interface DisplayWidgetTypesPanelData {
types: WidgetTypes[];
typesUpdated: (columns: WidgetTypes[]) => void;
}
@Component({
selector: 'tb-widget-types-panel',
templateUrl: './widget-types-panel.component.html',
styleUrls: ['./widget-types-panel.component.scss']
})
export class DisplayWidgetTypesPanelComponent {
types: WidgetTypes[];
constructor(@Inject(DISPLAY_WIDGET_TYPES_PANEL_DATA) public data: DisplayWidgetTypesPanelData) {
this.types = this.data.types;
}
public update() {
this.data.typesUpdated(this.types);
}
}

20
ui-ngx/src/app/modules/home/components/details-panel.component.html

@ -18,13 +18,27 @@
<header> <header>
<mat-toolbar class="details-toolbar" color="primary" [ngStyle]="{height: headerHeightPx+'px'}"> <mat-toolbar class="details-toolbar" color="primary" [ngStyle]="{height: headerHeightPx+'px'}">
<div class="mat-toolbar-tools" fxLayout="row" fxLayoutAlign="start center" style="height: 100%;"> <div class="mat-toolbar-tools" fxLayout="row" fxLayoutAlign="start center" style="height: 100%;">
<div class="mat-toolbar-tools tb-details-title-header" fxFlex fxLayout="column" fxLayoutAlign="start start"> <div class="mat-toolbar-tools tb-details-title-header" fxFlex fxLayout="column" fxLayoutAlign="start start" [fxHide]="showSearchPane">
<span class="tb-details-title">{{ headerTitle }}</span> <div class="tb-details-title" fxLayout="row" fxLayoutAlign="start center">
<ng-content select=".prefix-title-buttons"></ng-content>
<span>{{ headerTitle }}</span>
</div>
<span class="tb-details-subtitle">{{ headerSubtitle }}</span> <span class="tb-details-subtitle">{{ headerSubtitle }}</span>
<span style="width: 100%;"> <span style="width: 100%;">
<ng-content select=".header-pane"></ng-content> <ng-content select=".header-pane"></ng-content>
</span> </span>
</div> </div>
<div *ngIf="isShowSearch" [fxHide]="!showSearchPane" fxLayout="row" fxLayoutAlign="start center" style="width: 100%;">
<button mat-button mat-icon-button (click)="onToggleSearch()">
<mat-icon class="material-icons">arrow_back</mat-icon>
</button>
<div style="width: 100%; margin: 0 20px">
<ng-content select=".search-pane"></ng-content>
</div>
</div>
<button mat-button mat-icon-button *ngIf="isShowSearch && !showSearchPane" (click)="onToggleSearch()">
<mat-icon class="material-icons">search</mat-icon>
</button>
<ng-content select=".details-buttons"></ng-content> <ng-content select=".details-buttons"></ng-content>
<button mat-button mat-icon-button (click)="onCloseDetails()"> <button mat-button mat-icon-button (click)="onCloseDetails()">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
@ -51,6 +65,6 @@
</section> </section>
</mat-toolbar> </mat-toolbar>
</header> </header>
<div fxFlex class="mat-content"> <div fxFlex class="mat-content" [ngStyle]="{backgroundColor: backgroundColor}">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>

2
ui-ngx/src/app/modules/home/components/details-panel.component.scss

@ -28,7 +28,7 @@
:host ::ng-deep { :host ::ng-deep {
.mat-toolbar-tools { .mat-toolbar-tools {
height: 100%; height: 100%;
min-height: 100px; min-height: 60px;
max-height: 120px; max-height: 120px;
&.tb-details-title-header { &.tb-details-title-header {
min-width: 0; min-width: 0;

11
ui-ngx/src/app/modules/home/components/details-panel.component.ts

@ -32,6 +32,8 @@ export class DetailsPanelComponent extends PageComponent {
@Input() headerSubtitle = ''; @Input() headerSubtitle = '';
@Input() isReadOnly = false; @Input() isReadOnly = false;
@Input() isAlwaysEdit = false; @Input() isAlwaysEdit = false;
@Input() isShowSearch = false;
@Input() backgroundColor = '#FFF';
theFormValue: FormGroup; theFormValue: FormGroup;
@ -50,8 +52,11 @@ export class DetailsPanelComponent extends PageComponent {
toggleDetailsEditMode = new EventEmitter<boolean>(); toggleDetailsEditMode = new EventEmitter<boolean>();
@Output() @Output()
applyDetails = new EventEmitter<void>(); applyDetails = new EventEmitter<void>();
@Output()
closeSearch = new EventEmitter<void>();
isEditValue = false; isEditValue = false;
showSearchPane = false;
@Output() @Output()
isEditChange = new EventEmitter<boolean>(); isEditChange = new EventEmitter<boolean>();
@ -88,4 +93,10 @@ export class DetailsPanelComponent extends PageComponent {
} }
} }
onToggleSearch() {
this.showSearchPane = !this.showSearchPane;
if (!this.showSearchPane) {
this.closeSearch.emit();
}
}
} }

7
ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts

@ -25,7 +25,6 @@ import {
Injector, Injector,
Input, Input,
OnDestroy, OnDestroy,
OnInit,
Output, Output,
QueryList, QueryList,
ViewChild, ViewChild,
@ -52,7 +51,7 @@ import { deepClone, mergeDeep } from '@core/utils';
styleUrls: ['./entity-details-panel.component.scss'], styleUrls: ['./entity-details-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class EntityDetailsPanelComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { export class EntityDetailsPanelComponent extends PageComponent implements AfterViewInit, OnDestroy {
@Output() @Output()
closeEntityDetails = new EventEmitter<void>(); closeEntityDetails = new EventEmitter<void>();
@ -140,10 +139,6 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
return this.isEditValue; return this.isEditValue;
} }
ngOnInit(): void {
this.init();
}
private init() { private init() {
this.translations = this.entitiesTableConfig.entityTranslations; this.translations = this.entitiesTableConfig.entityTranslations;
this.resources = this.entitiesTableConfig.entityResources; this.resources = this.entitiesTableConfig.entityResources;

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

@ -132,6 +132,7 @@ import { ManageDashboardStatesDialogComponent } from '@home/components/dashboard
import { DashboardStateDialogComponent } from '@home/components/dashboard-page/states/dashboard-state-dialog.component'; import { DashboardStateDialogComponent } from '@home/components/dashboard-page/states/dashboard-state-dialog.component';
import { EmbedDashboardDialogComponent } from '@home/components/widget/dialog/embed-dashboard-dialog.component'; import { EmbedDashboardDialogComponent } from '@home/components/widget/dialog/embed-dashboard-dialog.component';
import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token';
import { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -244,7 +245,8 @@ import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/emb
DashboardSettingsDialogComponent, DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent, DashboardStateDialogComponent,
EmbedDashboardDialogComponent EmbedDashboardDialogComponent,
DisplayWidgetTypesPanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -345,7 +347,8 @@ import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/emb
DashboardSettingsDialogComponent, DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent, DashboardStateDialogComponent,
EmbedDashboardDialogComponent EmbedDashboardDialogComponent,
DisplayWidgetTypesPanelComponent
], ],
providers: [ providers: [
WidgetComponentService, WidgetComponentService,

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

@ -274,6 +274,7 @@ export class ImportExportService {
const isSystem = widgetsBundle.tenantId.id === NULL_UUID; const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe( this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe(
(widgetTypes) => { (widgetTypes) => {
widgetTypes = widgetTypes.sort((a, b) => a.createdTime - b.createdTime);
const widgetsBundleItem: WidgetsBundleItem = { const widgetsBundleItem: WidgetsBundleItem = {
widgetsBundle: this.prepareExport(widgetsBundle), widgetsBundle: this.prepareExport(widgetsBundle),
widgetTypes: [] widgetTypes: []

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

@ -104,8 +104,10 @@ export class WidgetComponentService {
controllerScript: this.utils.editWidgetInfo.controllerScript, controllerScript: this.utils.editWidgetInfo.controllerScript,
settingsSchema: this.utils.editWidgetInfo.settingsSchema, settingsSchema: this.utils.editWidgetInfo.settingsSchema,
dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
image: this.utils.editWidgetInfo.image,
description: this.utils.editWidgetInfo.description,
defaultConfig: this.utils.editWidgetInfo.defaultConfig defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle' }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle', undefined
); );
} }
const initSubject = new ReplaySubject(); const initSubject = new ReplaySubject();

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

@ -347,6 +347,8 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
alias: string; alias: string;
typeSettingsSchema?: string | any; typeSettingsSchema?: string | any;
typeDataKeySettingsSchema?: string | any; typeDataKeySettingsSchema?: string | any;
image: string;
description: string;
componentFactory?: ComponentFactory<IDynamicWidgetComponent>; componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
} }
@ -375,6 +377,8 @@ export const MissingWidgetType: WidgetInfo = {
controllerScript: 'self.onInit = function() {}', controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n', settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n', dataKeySettingsSchema: '{}\n',
image: null,
description: null,
defaultConfig: '{\n' + defaultConfig: '{\n' +
'"title": "Widget type not found",\n' + '"title": "Widget type not found",\n' +
'"datasources": [],\n' + '"datasources": [],\n' +
@ -398,6 +402,8 @@ export const ErrorWidgetType: WidgetInfo = {
controllerScript: 'self.onInit = function() {}', controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n', settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n', dataKeySettingsSchema: '{}\n',
image: null,
description: null,
defaultConfig: '{\n' + defaultConfig: '{\n' +
'"title": "Widget failed to load",\n' + '"title": "Widget failed to load",\n' +
'"datasources": [],\n' + '"datasources": [],\n' +
@ -424,6 +430,8 @@ export interface WidgetTypeInstance {
export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
return { return {
widgetName: widgetTypeEntity.name, widgetName: widgetTypeEntity.name,
image: widgetTypeEntity.image,
description: widgetTypeEntity.description,
alias: widgetTypeEntity.alias, alias: widgetTypeEntity.alias,
type: widgetTypeEntity.descriptor.type, type: widgetTypeEntity.descriptor.type,
sizeX: widgetTypeEntity.descriptor.sizeX, sizeX: widgetTypeEntity.descriptor.sizeX,
@ -438,7 +446,8 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
}; };
} }
export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, bundleAlias: string): WidgetType { export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
bundleAlias: string, createdTime: number): WidgetType {
const descriptor: WidgetTypeDescriptor = { const descriptor: WidgetTypeDescriptor = {
type: widgetInfo.type, type: widgetInfo.type,
sizeX: widgetInfo.sizeX, sizeX: widgetInfo.sizeX,
@ -454,9 +463,12 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
return { return {
id, id,
tenantId, tenantId,
createdTime,
bundleAlias, bundleAlias,
alias: widgetInfo.alias, alias: widgetInfo.alias,
name: widgetInfo.widgetName, name: widgetInfo.widgetName,
image: widgetInfo.image,
description: widgetInfo.description,
descriptor descriptor
}; };
} }

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

@ -117,7 +117,7 @@
<div #topPanel class="tb-split tb-split-vertical"> <div #topPanel class="tb-split tb-split-vertical">
<div #topLeftPanel class="tb-split tb-content"> <div #topLeftPanel class="tb-split tb-content">
<mat-tab-group selectedIndex="1" dynamicHeight="true" style="width: 100%; height: 100%;"> <mat-tab-group selectedIndex="1" dynamicHeight="true" style="width: 100%; height: 100%;">
<mat-tab label="{{ 'widget.resources' | translate }}" style="width: 100%; height: 100%;"> <mat-tab label="{{ 'widget.resources' | translate }}">
<div class="tb-resize-container" style="background-color: #fff;"> <div class="tb-resize-container" style="background-color: #fff;">
<div class="mat-padding"> <div class="mat-padding">
<div fxFlex fxLayout="row" style="max-height: 40px;" <div fxFlex fxLayout="row" style="max-height: 40px;"
@ -153,7 +153,7 @@
</div> </div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'widget.html' | translate }}" style="width: 100%; height: 100%;"> <mat-tab label="{{ 'widget.html' | translate }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="htmlFullscreen"> <div class="tb-resize-container" tb-fullscreen [fullscreen]="htmlFullscreen">
<div class="tb-editor-area-title-panel"> <div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyHtml()"> <button mat-button (click)="beautifyHtml()">
@ -169,7 +169,7 @@
<div #htmlInput></div> <div #htmlInput></div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;"> <mat-tab label="{{ 'widget.css' | translate }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="cssFullscreen"> <div class="tb-resize-container" tb-fullscreen [fullscreen]="cssFullscreen">
<div class="tb-editor-area-title-panel"> <div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyCss()"> <button mat-button (click)="beautifyCss()">
@ -189,7 +189,7 @@
</div> </div>
<div #topRightPanel class="tb-split tb-content"> <div #topRightPanel class="tb-split tb-content">
<mat-tab-group dynamicHeight="true" style="width: 100%; height: 100%;"> <mat-tab-group dynamicHeight="true" style="width: 100%; height: 100%;">
<mat-tab label="{{ 'widget.settings-schema' | translate }}" style="width: 100%; height: 100%;"> <mat-tab label="{{ 'widget.settings-schema' | translate }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen"> <div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen">
<div class="tb-editor-area-title-panel"> <div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyJson()"> <button mat-button (click)="beautifyJson()">
@ -205,7 +205,7 @@
<div #settingsJsonInput></div> <div #settingsJsonInput></div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'widget.datakey-settings-schema' | translate }}" style="width: 100%; height: 100%;"> <mat-tab label="{{ 'widget.datakey-settings-schema' | translate }}">
<div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonDataKeySettingsFullscreen"> <div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonDataKeySettingsFullscreen">
<div class="tb-editor-area-title-panel"> <div class="tb-editor-area-title-panel">
<button mat-button (click)="beautifyDataKeyJson()"> <button mat-button (click)="beautifyDataKeyJson()">
@ -221,6 +221,25 @@
<div #dataKeySettingsJsonInput></div> <div #dataKeySettingsJsonInput></div>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'widget.widget-settings' | translate }}">
<div class="tb-resize-container" style="background-color: #fff;">
<div class="mat-padding">
<tb-image-input fxFlex label="{{'widget.image-preview' | translate}}"
maxSizeByte="524288"
[(ngModel)]="widget.image"
(ngModelChange)="isDirty = true" >
</tb-image-input>
<mat-form-field class="mat-block">
<mat-label translate>widget.description</mat-label>
<textarea matInput #descriptionInput
[(ngModel)]="widget.description"
(ngModelChange)="isDirty = true"
rows="2" maxlength="255"></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint>
</mat-form-field>
</div>
</div>
</mat-tab>
</mat-tab-group> </mat-tab-group>
</div> </div>
</div> </div>

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

@ -516,7 +516,8 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
private commitSaveWidget() { private commitSaveWidget() {
const id = (this.widgetType && this.widgetType.id) ? this.widgetType.id : undefined; const id = (this.widgetType && this.widgetType.id) ? this.widgetType.id : undefined;
this.widgetService.saveWidgetType(this.widget, id, this.widgetsBundle.alias).subscribe( const createdTime = (this.widgetType && this.widgetType.createdTime) ? this.widgetType.createdTime : undefined;
this.widgetService.saveWidgetType(this.widget, id, this.widgetsBundle.alias, createdTime).subscribe(
(widgetTypeInstance) => { (widgetTypeInstance) => {
this.setWidgetType(widgetTypeInstance); this.setWidgetType(widgetTypeInstance);
this.saveWidgetPending = false; this.saveWidgetPending = false;
@ -543,7 +544,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
config.title = this.widget.widgetName; config.title = this.widget.widgetName;
this.widget.defaultConfig = JSON.stringify(config); this.widget.defaultConfig = JSON.stringify(config);
this.isDirty = false; this.isDirty = false;
this.widgetService.saveWidgetType(this.widget, undefined, saveWidgetAsData.bundleAlias).subscribe( this.widgetService.saveWidgetType(this.widget, undefined, saveWidgetAsData.bundleAlias, undefined).subscribe(
(widgetTypeInstance) => { (widgetTypeInstance) => {
this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeInstance.id.id}`); this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeInstance.id.id}`);
} }

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

@ -45,6 +45,16 @@
{{ 'widgets-bundle.title-required' | translate }} {{ 'widgets-bundle.title-required' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<tb-image-input fxFlex
label="{{'widgets-bundle.image-preview' | translate}}"
maxSizeByte="524288"
formControlName="image">
</tb-image-input>
<mat-form-field class="mat-block">
<mat-label translate>widgets-bundle.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>
</fieldset> </fieldset>
</form> </form>
</div> </div>

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

@ -47,12 +47,18 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> {
buildForm(entity: WidgetsBundle): FormGroup { buildForm(entity: WidgetsBundle): FormGroup {
return this.fb.group( return this.fb.group(
{ {
title: [entity ? entity.title : '', [Validators.required]] title: [entity ? entity.title : '', [Validators.required]],
image: [entity ? entity.image : ''],
description: [entity ? entity.description : '', Validators.maxLength(255)]
} }
); );
} }
updateForm(entity: WidgetsBundle) { updateForm(entity: WidgetsBundle) {
this.entityForm.patchValue({title: entity.title}); this.entityForm.patchValue({
title: entity.title,
image: entity.image,
description: entity.description
});
} }
} }

1
ui-ngx/src/app/shared/components/image-input.component.html

@ -42,4 +42,5 @@
</div> </div>
</div> </div>
</ng-container> </ng-container>
<div class="tb-hint" *ngIf="maxSizeByte" translate [translateParams]="{ size: maxSizeByte | fileSize}">dashboard.maximum-upload-file-size</div>
</div> </div>

4
ui-ngx/src/app/shared/components/image-input.component.scss

@ -101,4 +101,8 @@ $previewSize: 100px !default;
} }
} }
} }
.tb-hint{
margin-top: 8px;
}
} }

20
ui-ngx/src/app/shared/components/image-input.component.ts

@ -24,6 +24,9 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FlowDirective } from '@flowjs/ngx-flow'; import { FlowDirective } from '@flowjs/ngx-flow';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { FileSizePipe } from '@shared/pipe/file-size.pipe';
@Component({ @Component({
selector: 'tb-image-input', selector: 'tb-image-input',
@ -42,6 +45,9 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
@Input() @Input()
label: string; label: string;
@Input()
maxSizeByte: number;
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
@ -80,7 +86,10 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private utils: UtilsService, private utils: UtilsService,
private sanitizer: DomSanitizer) { private sanitizer: DomSanitizer,
private dialog: DialogService,
private translate: TranslateService,
private fileSize: FileSizePipe) {
super(store); super(store);
} }
@ -88,6 +97,15 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
this.autoUploadSubscription = this.flow.events$.subscribe(event => { this.autoUploadSubscription = this.flow.events$.subscribe(event => {
if (event.type === 'fileAdded') { if (event.type === 'fileAdded') {
const file = (event.event[0] as flowjs.FlowFile).file; const file = (event.event[0] as flowjs.FlowFile).file;
if (this.maxSizeByte && this.maxSizeByte < file.size) {
this.dialog.alert(
this.translate.instant('dashboard.cannot-upload-file'),
this.translate.instant('dashboard.maximum-upload-file-size', {size: this.fileSize.transform(this.maxSizeByte)})
).subscribe(
() => { }
);
return false;
}
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (loadEvent) => { reader.onload = (loadEvent) => {
if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) {

29
ui-ngx/src/app/shared/components/widgets-bundle-search.component.html

@ -0,0 +1,29 @@
<!--
Copyright © 2016-2021 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="input-wrapper" [ngClass]="{'focus': focus}" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-icon>search</mat-icon>
<input type="text"
[(ngModel)]="searchText"
(ngModelChange)="updateSearchText()"
(focus)="toggleFocus()"
(blur)="toggleFocus()"
[placeholder]=placeholder>
<button mat-button type="button" *ngIf="searchText" mat-icon-button (click)="clear($event)">
<mat-icon class="close">close</mat-icon>
</button>
</div>

55
ui-ngx/src/app/shared/components/widgets-bundle-search.component.scss

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2021 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.
*/
.input-wrapper {
background: hsla(0, 0%, 100%, .2);
border-radius: 8px;
color: rgb(255, 255, 255, .84);
padding: 5px 8px 5px 16px;
height: 46px;
input {
width: 100%;
height: 100%;
padding: 0;
font-size: 20px;
outline: none;
border: none;
background-color: transparent;
color: #fff;
&::placeholder {
color: #fff;
opacity: .6;
line-height: 26px;
}
}
&.focus {
background: #fff;
color: rgba(0,0,0,.54);
input {
color: #000;
opacity: .60;
}
.close {
color: #000 !important;
opacity: .54;
}
}
}

74
ui-ngx/src/app/shared/components/widgets-bundle-search.component.ts

@ -0,0 +1,74 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
selector: 'tb-widgets-bundle-search',
templateUrl: './widgets-bundle-search.component.html',
styleUrls: ['./widgets-bundle-search.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetsBundleSearchComponent),
multi: true
}],
encapsulation: ViewEncapsulation.None
})
export class WidgetsBundleSearchComponent implements ControlValueAccessor {
searchText: string;
focus = false;
@Input() placeholder: string;
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
private propagateChange = (v: any) => { };
constructor() {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
writeValue(value: string | null): void {
this.searchText = value;
}
updateSearchText(): void {
this.updateView();
}
private updateView() {
this.propagateChange(this.searchText);
}
clear($event: Event): void {
$event.preventDefault();
$event.stopPropagation();
this.searchText = '';
this.updateView();
}
toggleFocus() {
this.focus = !this.focus;
}
}

1
ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss

@ -89,6 +89,7 @@ mat-toolbar {
.mat-select-placeholder { .mat-select-placeholder {
color: #fff; color: #fff;
opacity: .8; opacity: .8;
line-height: 26px;
} }
} }
} }

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

@ -63,7 +63,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
[ [
widgetType.latest, widgetType.latest,
{ {
name: 'widget.latest-values', name: 'widget.latest',
icon: 'track_changes', icon: 'track_changes',
configHelpLinkId: 'widgetsConfigLatest', configHelpLinkId: 'widgetsConfigLatest',
template: { template: {
@ -170,6 +170,8 @@ export interface WidgetType extends BaseData<WidgetTypeId> {
bundleAlias: string; bundleAlias: string;
alias: string; alias: string;
name: string; name: string;
image: string;
description: string;
descriptor: WidgetTypeDescriptor; descriptor: WidgetTypeDescriptor;
} }
@ -399,14 +401,8 @@ export interface WidgetConfig {
[key: string]: any; [key: string]: any;
} }
export interface Widget { export interface Widget extends WidgetInfo{
id?: string;
typeId?: WidgetTypeId; typeId?: WidgetTypeId;
isSystemType: boolean;
bundleAlias: string;
typeAlias: string;
type: widgetType;
title: string;
sizeX: number; sizeX: number;
sizeY: number; sizeY: number;
row: number; row: number;
@ -414,6 +410,17 @@ export interface Widget {
config: WidgetConfig; config: WidgetConfig;
} }
export interface WidgetInfo {
id?: string;
isSystemType: boolean;
bundleAlias: string;
typeAlias: string;
type: widgetType;
title: string;
image: string;
description: string;
}
export interface GroupInfo { export interface GroupInfo {
formIndex: number; formIndex: number;
GroupTitle: string; GroupTitle: string;
@ -429,7 +436,7 @@ export interface JsonSchema {
export interface JsonSettingsSchema { export interface JsonSettingsSchema {
schema?: JsonSchema; schema?: JsonSchema;
form?: any[]; form?: any[];
groupInfoes?: GroupInfo[] groupInfoes?: GroupInfo[];
} }
export interface WidgetPosition { export interface WidgetPosition {

1
ui-ngx/src/app/shared/models/widgets-bundle.model.ts

@ -23,4 +23,5 @@ export interface WidgetsBundle extends BaseData<WidgetsBundleId> {
alias: string; alias: string;
title: string; title: string;
image: string; image: string;
description: string;
} }

56
ui-ngx/src/app/shared/pipe/file-size.pipe.ts

@ -0,0 +1,56 @@
///
/// Copyright © 2016-2021 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 { Pipe, PipeTransform } from '@angular/core';
type unit = 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB';
type unitPrecisionMap = {
[u in unit]: number;
};
const defaultPrecisionMap: unitPrecisionMap = {
bytes: 0,
KB: 0,
MB: 1,
GB: 1,
TB: 2,
PB: 2
};
@Pipe({ name: 'fileSize' })
export class FileSizePipe implements PipeTransform {
private readonly units: unit[] = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
transform(bytes: number = 0, precision: number | unitPrecisionMap = defaultPrecisionMap): string {
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) {
return '?';
}
let unitIndex = 0;
while (bytes >= 1024) {
bytes /= 1024;
unitIndex++;
}
const unitSymbol = this.units[unitIndex];
if (typeof precision === 'number') {
return `${bytes.toFixed(+precision)} ${unitSymbol}`;
}
return `${bytes.toFixed(precision[unitSymbol])} ${unitSymbol}`;
}
}

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

@ -20,3 +20,4 @@ export * from './keyboard-shortcut.pipe';
export * from './milliseconds-to-time-string.pipe'; export * from './milliseconds-to-time-string.pipe';
export * from './nospace.pipe'; export * from './nospace.pipe';
export * from './truncate.pipe'; export * from './truncate.pipe';
export * from './file-size.pipe';

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

@ -137,6 +137,8 @@ import { DndModule } from 'ngx-drag-drop';
import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component';
import { ContactComponent } from '@shared/components/contact.component'; import { ContactComponent } from '@shared/components/contact.component';
import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component';
import { FileSizePipe } from '@shared/pipe/file-size.pipe';
import { WidgetsBundleSearchComponent } from '@shared/components/widgets-bundle-search.component';
@NgModule({ @NgModule({
providers: [ providers: [
@ -146,6 +148,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
HighlightPipe, HighlightPipe,
TruncatePipe, TruncatePipe,
TbJsonPipe, TbJsonPipe,
FileSizePipe,
{ {
provide: FlowInjectionToken, provide: FlowInjectionToken,
useValue: Flow useValue: Flow
@ -219,12 +222,14 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
HighlightPipe, HighlightPipe,
TruncatePipe, TruncatePipe,
TbJsonPipe, TbJsonPipe,
FileSizePipe,
KeyboardShortcutPipe, KeyboardShortcutPipe,
TbJsonToStringDirective, TbJsonToStringDirective,
JsonObjectEditDialogComponent, JsonObjectEditDialogComponent,
HistorySelectorComponent, HistorySelectorComponent,
EntityGatewaySelectComponent, EntityGatewaySelectComponent,
ContactComponent ContactComponent,
WidgetsBundleSearchComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -387,11 +392,13 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
TruncatePipe, TruncatePipe,
TbJsonPipe, TbJsonPipe,
KeyboardShortcutPipe, KeyboardShortcutPipe,
FileSizePipe,
TranslateModule, TranslateModule,
JsonObjectEditDialogComponent, JsonObjectEditDialogComponent,
HistorySelectorComponent, HistorySelectorComponent,
EntityGatewaySelectComponent, EntityGatewaySelectComponent,
ContactComponent ContactComponent,
WidgetsBundleSearchComponent
] ]
}) })
export class SharedModule { } export class SharedModule { }

2
ui-ngx/src/assets/locale/locale.constant-cs_CZ.json

@ -2206,7 +2206,7 @@
"timeseries": "Časové řady", "timeseries": "Časové řady",
"search-data": "Vyhledat data", "search-data": "Vyhledat data",
"no-data-found": "Žádná data nebyla nalezena", "no-data-found": "Žádná data nebyla nalezena",
"latest-values": "Poslední hodnoty", "latest": "Poslední hodnoty",
"rpc": "Ovládací widget", "rpc": "Ovládací widget",
"alarm": "Widgety alarmu", "alarm": "Widgety alarmu",
"static": "Statické widgety", "static": "Statické widgety",

2
ui-ngx/src/assets/locale/locale.constant-de_DE.json

@ -1451,7 +1451,7 @@
"timeseries": "Zeitreihe", "timeseries": "Zeitreihe",
"search-data": "Daten suchen", "search-data": "Daten suchen",
"no-data-found": "Keine Daten gefunden", "no-data-found": "Keine Daten gefunden",
"latest-values": "Neueste Werte", "latest": "Neueste Werte",
"rpc": "Steuerungswidget", "rpc": "Steuerungswidget",
"alarm": "Alarm-Widget", "alarm": "Alarm-Widget",
"static": "Statisches Widget", "static": "Statisches Widget",

2
ui-ngx/src/assets/locale/locale.constant-el_GR.json

@ -2316,7 +2316,7 @@
"timeseries": "Χρονική σειρά", "timeseries": "Χρονική σειρά",
"search-data": "Αναζήτηση δεδομένων", "search-data": "Αναζήτηση δεδομένων",
"no-data-found": "Δεν βρέθηκαν δεδομένα", "no-data-found": "Δεν βρέθηκαν δεδομένα",
"latest-values": "Τελευταίες αξίες", "latest": "Τελευταίες αξίες",
"rpc": "Έλεγχος Widget", "rpc": "Έλεγχος Widget",
"alarm": "Alarm widget", "alarm": "Alarm widget",
"static": "Στατικό widget", "static": "Στατικό widget",

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

@ -646,6 +646,7 @@
"add-widget": "Add new widget", "add-widget": "Add new widget",
"title": "Title", "title": "Title",
"select-widget-title": "Select widget", "select-widget-title": "Select widget",
"select-widget-value": "{{title}}: select widget",
"select-widget-subtitle": "List of available widget types", "select-widget-subtitle": "List of available widget types",
"delete": "Delete dashboard", "delete": "Delete dashboard",
"title-required": "Title is required.", "title-required": "Title is required.",
@ -691,6 +692,8 @@
"background-size-mode": "Background size mode", "background-size-mode": "Background size mode",
"no-image": "No image selected", "no-image": "No image selected",
"drop-image": "Drop an image or click to select a file to upload.", "drop-image": "Drop an image or click to select a file to upload.",
"maximum-upload-file-size": "Maximum upload file size: {{ size }}",
"cannot-upload-file": "Cannot upload file",
"settings": "Settings", "settings": "Settings",
"columns-count": "Columns count", "columns-count": "Columns count",
"columns-count-required": "Columns count is required.", "columns-count-required": "Columns count is required.",
@ -2205,6 +2208,7 @@
"widget": { "widget": {
"widget-library": "Widgets Library", "widget-library": "Widgets Library",
"widget-bundle": "Widgets Bundle", "widget-bundle": "Widgets Bundle",
"all-bundles": "All bundles",
"select-widgets-bundle": "Select widgets bundle", "select-widgets-bundle": "Select widgets bundle",
"management": "Widget management", "management": "Widget management",
"editor": "Widget Editor", "editor": "Widget Editor",
@ -2217,7 +2221,7 @@
"timeseries": "Time series", "timeseries": "Time series",
"search-data": "Search data", "search-data": "Search data",
"no-data-found": "No data found", "no-data-found": "No data found",
"latest-values": "Latest values", "latest": "Latest values",
"rpc": "Control widget", "rpc": "Control widget",
"alarm": "Alarm widget", "alarm": "Alarm widget",
"static": "Static widget", "static": "Static widget",
@ -2244,6 +2248,9 @@
"css": "CSS", "css": "CSS",
"settings-schema": "Settings schema", "settings-schema": "Settings schema",
"datakey-settings-schema": "Data key settings schema", "datakey-settings-schema": "Data key settings schema",
"widget-settings": "Widget settings",
"description": "Description",
"image-preview": "Image preview",
"javascript": "Javascript", "javascript": "Javascript",
"js": "JS", "js": "JS",
"remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?",
@ -2257,7 +2264,9 @@
"export": "Export widget", "export": "Export widget",
"no-data": "No data to display on widget", "no-data": "No data to display on widget",
"data-overflow": "Widget displays {{count}} out of {{total}} entities", "data-overflow": "Widget displays {{count}} out of {{total}} entities",
"alarm-data-overflow": "Widget displays alarms for {{allowedEntities}} (maximum allowed) entities out of {{totalEntities}} entities" "alarm-data-overflow": "Widget displays alarms for {{allowedEntities}} (maximum allowed) entities out of {{totalEntities}} entities",
"search": "Search widget",
"filter": "Widget filter type"
}, },
"widget-action": { "widget-action": {
"header-button": "Widget header button", "header-button": "Widget header button",
@ -2286,6 +2295,8 @@
"delete": "Delete widgets bundle", "delete": "Delete widgets bundle",
"title": "Title", "title": "Title",
"title-required": "Title is required.", "title-required": "Title is required.",
"description": "Description",
"image-preview": "Image preview",
"add-widgets-bundle-text": "Add new widgets bundle", "add-widgets-bundle-text": "Add new widgets bundle",
"no-widgets-bundles-text": "No widgets bundles found", "no-widgets-bundles-text": "No widgets bundles found",
"empty": "Widgets bundle is empty", "empty": "Widgets bundle is empty",

2
ui-ngx/src/assets/locale/locale.constant-es_ES.json

@ -2206,7 +2206,7 @@
"timeseries": "Series de tiempo", "timeseries": "Series de tiempo",
"search-data": "Buscar datos", "search-data": "Buscar datos",
"no-data-found": "No se han encontrado datos", "no-data-found": "No se han encontrado datos",
"latest-values": "Últimos valores", "latest": "Últimos valores",
"rpc": "Widget de control", "rpc": "Widget de control",
"alarm": "Widget de Alarma", "alarm": "Widget de Alarma",
"static": "Widget estático", "static": "Widget estático",

2
ui-ngx/src/assets/locale/locale.constant-fa_IR.json

@ -1420,7 +1420,7 @@
"timeseries": "سري هاي زماني", "timeseries": "سري هاي زماني",
"search-data": "جستجوي داده", "search-data": "جستجوي داده",
"no-data-found": "هيچ داده اي يافت نشد", "no-data-found": "هيچ داده اي يافت نشد",
"latest-values": "آخرين مقادير", "latest": "آخرين مقادير",
"rpc": "ويجت کنترل", "rpc": "ويجت کنترل",
"alarm": "ويجت هشدار", "alarm": "ويجت هشدار",
"static": "ويجت ايستا", "static": "ويجت ايستا",

2
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

@ -1494,7 +1494,7 @@
"export": "Exporter widget", "export": "Exporter widget",
"html": "HTML", "html": "HTML",
"javascript": "Javascript", "javascript": "Javascript",
"latest-values": "Dernières valeurs", "latest": "Dernières valeurs",
"management": "Gestion des widgets", "management": "Gestion des widgets",
"missing-widget-title-error": "Le titre du widget doit être spécifié!", "missing-widget-title-error": "Le titre du widget doit être spécifié!",
"no-data-found": "Aucune donnée trouvée", "no-data-found": "Aucune donnée trouvée",

2
ui-ngx/src/assets/locale/locale.constant-it_IT.json

@ -1463,7 +1463,7 @@
"timeseries": "Time series", "timeseries": "Time series",
"search-data": "Cerca dati", "search-data": "Cerca dati",
"no-data-found": "Nessun dato trovato", "no-data-found": "Nessun dato trovato",
"latest-values": "Ultimi valori", "latest": "Ultimi valori",
"rpc": "Control widget", "rpc": "Control widget",
"alarm": "Alarm widget", "alarm": "Alarm widget",
"static": "Static widget", "static": "Static widget",

2
ui-ngx/src/assets/locale/locale.constant-ja_JA.json

@ -1305,7 +1305,7 @@
"timeseries": "時系列", "timeseries": "時系列",
"search-data": "検索データ", "search-data": "検索データ",
"no-data-found": "何もデータが見つかりませんでした", "no-data-found": "何もデータが見つかりませんでした",
"latest-values": "最新の値", "latest": "最新の値",
"rpc": "コントロールウィジェット", "rpc": "コントロールウィジェット",
"alarm": "アラームウィジェット", "alarm": "アラームウィジェット",
"static": "静的ウィジェット", "static": "静的ウィジェット",

2
ui-ngx/src/assets/locale/locale.constant-ka_GE.json

@ -1551,7 +1551,7 @@
"timeseries": "მონაცემთა სერია", "timeseries": "მონაცემთა სერია",
"search-data": "საძიებო მონაცემები", "search-data": "საძიებო მონაცემები",
"no-data-found": "მონაცემი ვერ მოიძებნა", "no-data-found": "მონაცემი ვერ მოიძებნა",
"latest-values": "უახლესი მნიშვნელობები", "latest": "უახლესი მნიშვნელობები",
"rpc": "ვიჯეტის კონტროლი", "rpc": "ვიჯეტის კონტროლი",
"alarm": "ვიჯეტის განგაში", "alarm": "ვიჯეტის განგაში",
"static": "სტატიკური ვიჯეტი", "static": "სტატიკური ვიჯეტი",

2
ui-ngx/src/assets/locale/locale.constant-ko_KR.json

@ -2202,7 +2202,7 @@
"timeseries": "시계열", "timeseries": "시계열",
"search-data": "데이터 검색", "search-data": "데이터 검색",
"no-data-found": "아무 데이터도 없습니다", "no-data-found": "아무 데이터도 없습니다",
"latest-values": "최근 값", "latest": "최근 값",
"rpc": "컨트롤 위젯", "rpc": "컨트롤 위젯",
"alarm": "알람 위젯", "alarm": "알람 위젯",
"static": "상태 위젯", "static": "상태 위젯",

2
ui-ngx/src/assets/locale/locale.constant-lv_LV.json

@ -1468,7 +1468,7 @@
"timeseries": "Laika sērijas", "timeseries": "Laika sērijas",
"search-data": "Meklēt datus", "search-data": "Meklēt datus",
"no-data-found": "Nav datu atrasti", "no-data-found": "Nav datu atrasti",
"latest-values": "Pedējās vērtības", "latest": "Pedējās vērtības",
"rpc": "Kontroles logrīks", "rpc": "Kontroles logrīks",
"alarm": "Trauksmes logrīks", "alarm": "Trauksmes logrīks",
"static": "Statiskais logrīks", "static": "Statiskais logrīks",

2
ui-ngx/src/assets/locale/locale.constant-pt_BR.json

@ -1777,7 +1777,7 @@
"timeseries": "Intervalos de tempo", "timeseries": "Intervalos de tempo",
"search-data": "Pesquisar dados", "search-data": "Pesquisar dados",
"no-data-found": "Nenhum dado encontrado", "no-data-found": "Nenhum dado encontrado",
"latest-values": "Últimos valores", "latest": "Últimos valores",
"rpc": "Widget de controle", "rpc": "Widget de controle",
"alarm": "Widget de alarme", "alarm": "Widget de alarme",
"static": "Widget estático", "static": "Widget estático",

2
ui-ngx/src/assets/locale/locale.constant-ro_RO.json

@ -1535,7 +1535,7 @@
"timeseries": "Serii Temporale", "timeseries": "Serii Temporale",
"search-data": "Caută Date", "search-data": "Caută Date",
"no-data-found": "Nu Au Fost Găsite Date", "no-data-found": "Nu Au Fost Găsite Date",
"latest-values": "Ultimele Valori", "latest": "Ultimele Valori",
"rpc": "Widget Control ", "rpc": "Widget Control ",
"alarm": "Widget Alarmă", "alarm": "Widget Alarmă",
"static": "Widget Static", "static": "Widget Static",

2
ui-ngx/src/assets/locale/locale.constant-ru_RU.json

@ -1541,7 +1541,7 @@
"timeseries": "Телеметрия", "timeseries": "Телеметрия",
"search-data": "Поиск данных", "search-data": "Поиск данных",
"no-data-found": "Данные не найдено", "no-data-found": "Данные не найдено",
"latest-values": "Последние значения", "latest": "Последние значения",
"rpc": "Управляющий виджет", "rpc": "Управляющий виджет",
"alarm": "Виджет оповещений", "alarm": "Виджет оповещений",
"static": "Статический виджет", "static": "Статический виджет",

2
ui-ngx/src/assets/locale/locale.constant-sl_SI.json

@ -2202,7 +2202,7 @@
"timeseries": "Časovne serije", "timeseries": "Časovne serije",
"search-data": "Iskanje podatkov", "search-data": "Iskanje podatkov",
"no-data-found": "Podatkov ni mogoče najti", "no-data-found": "Podatkov ni mogoče najti",
"latest-values": "Najnovejše vrednosti", "latest": "Najnovejše vrednosti",
"rpc": "Nadzorni pripomoček", "rpc": "Nadzorni pripomoček",
"alarm": "Pripomoček za alarm", "alarm": "Pripomoček za alarm",
"static": "Statični pripomoček", "static": "Statični pripomoček",

2
ui-ngx/src/assets/locale/locale.constant-tr_TR.json

@ -1464,7 +1464,7 @@
"timeseries": "Zaman serisi", "timeseries": "Zaman serisi",
"search-data": "Arama verileri", "search-data": "Arama verileri",
"no-data-found": "Veri bulunamadı", "no-data-found": "Veri bulunamadı",
"latest-values": "Son değerler", "latest": "Son değerler",
"rpc": "Kontrol göstergesi", "rpc": "Kontrol göstergesi",
"alarm": "Alarm göstergesi", "alarm": "Alarm göstergesi",
"static": "Statik gösterge", "static": "Statik gösterge",

2
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

@ -2109,7 +2109,7 @@
"timeseries": "Телеметрія", "timeseries": "Телеметрія",
"search-data": "Пошук даних", "search-data": "Пошук даних",
"no-data-found": "Даних не знайдено", "no-data-found": "Даних не знайдено",
"latest-values": "Останні значення", "latest": "Останні значення",
"rpc": "Керуючий віджет", "rpc": "Керуючий віджет",
"alarm": "Віджет сигнала тривоги", "alarm": "Віджет сигнала тривоги",
"static": "Статичний віджет", "static": "Статичний віджет",

4
ui-ngx/src/assets/locale/locale.constant-zh_CN.json

@ -2323,7 +2323,7 @@
"html": "HTML", "html": "HTML",
"javascript": "Javascript", "javascript": "Javascript",
"js": "JS", "js": "JS",
"latest-values": "最新值", "latest": "最新值",
"management": "管理部件", "management": "管理部件",
"missing-widget-title-error": "部件标题必须指定!", "missing-widget-title-error": "部件标题必须指定!",
"no-data": "小部件上没有要显示的数据", "no-data": "小部件上没有要显示的数据",
@ -2500,4 +2500,4 @@
"value": "价值" "value": "价值"
} }
} }
} }

2
ui-ngx/src/assets/locale/locale.constant-zh_TW.json

@ -1396,7 +1396,7 @@
"timeseries": "時間序列", "timeseries": "時間序列",
"search-data": "搜尋資料", "search-data": "搜尋資料",
"no-data-found": "沒有找到資料", "no-data-found": "沒有找到資料",
"latest-values": "最新值", "latest": "最新值",
"rpc": "控件部件", "rpc": "控件部件",
"alarm": "警告部件", "alarm": "警告部件",
"static": "靜態部件", "static": "靜態部件",

1
ui-ngx/src/assets/widget-preview-empty.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

6
ui-ngx/src/styles.scss

@ -753,6 +753,9 @@ mat-label {
&.tb-mat-20 { &.tb-mat-20 {
@include tb-mat-icon-size(20); @include tb-mat-icon-size(20);
} }
&.tb-mat-28 {
@include tb-mat-icon-size(28);
}
&.tb-mat-32 { &.tb-mat-32 {
@include tb-mat-icon-size(32); @include tb-mat-icon-size(32);
} }
@ -762,6 +765,9 @@ mat-label {
} }
.mat-icon-button { .mat-icon-button {
&.tb-mat-28 {
@include tb-mat-icon-button-size(28);
}
&.tb-mat-32 { &.tb-mat-32 {
@include tb-mat-icon-button-size(32); @include tb-mat-icon-button-size(32);
} }

Loading…
Cancel
Save