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": {
"alias": "date",
"title": "Date",
"image": null
"image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAh0SURBVHja7d3/UxNnHsBx/7L7oXc/3NzM3dycvdajnavtUYXOVM/zsJ0eharIN5EgGLIEEWj40ihEEMSAhCU0CCoB0qAYhBKIfI+JBBJCstn3/UC9KtjT3g2apc/nh0x299nJvvLs83ye2X1mdx/xwB6IGPviARXNhxqI79sLDlAD+wLsiQjsC+4NSFBABERABERABERAdkKCblgf31bI3Xc79D//wuPJNwIZT5vFZ9xWqHRgoGB666vlZx9W0PvMQvXKa4MU5Ss+I7MGyTdxl6sriUagdJmlEial8qnxry6EFiTD98BY/YVog9TCiMVQpSiNerMTubw+BjS2ljqYqyh3B9pZ6pzqx1Ha1IxLMiwMf1GZGLtQ4aPB5NhtSH1Hh8/I2fVQcag8lt01fXkLQjb2jWUdZg+6YKQwHo72mxK+B+gXbU3UPRhopq3LV4ld3gjHc33xk9wORHMoiHeMulqD+cq0jpsJTw3SUvzM5mouuTO7XiP1ify7xujxiop8dKMOqfP+FiSRjdP4TT5mj3q8ouKUx9Td381qdd0pr83BtdHmMQa6+nIqSq+1mWZyFU4zbjRncsOti7taH5gJ6bBX1kpIS75aKIrkKrsPYfZzo3oqQYS2M2t1xfEtiMPMKTWUj9nD6RgRoL+bNifGLYjNjr3L1YSyCeQqnOZMNJbNY109rtbFcvy6WC6zEtLS6jkSObwWCC1GBosvdjBdxJgBoLS0pDbK1yVSJrcKgiNFVS1bkIlThpMum4Nro+FCY/5N9WJlmfcppPWsPiNB0UNcrZj0eh1lZeUFXNfFOvW6gdcAeRqJ+M510R8+ftymPD2e6GLM4oTN7aW3YibmMf24ezyexAkxUi+ZEz+5tdNYGXjdeUQMUQREQAREQAREQAREQATklwNZ7Otf3Vkq5gYIyRqC3OqeuhAgsUG0NIYSA2KJDRI+iBBoZENlAzUW1QDkDg9uzEm1dteXY1O1VRPQfqm2M1wTu9RkCjQOW6LVlqv+rGuagDxqjjmtBvQYuzq+hnYvZeEat4PJQG65OlzZd8rfiCYg8h2HfdWAHsOkdwXavZSGa1wOfAGdcXGw3ev1f6MFSHHtZfX+eZMOyTVhNLuh/WKNHK6JVV5uCjSuX1iTmts0AdmKuAoJBVUB2r1bVz2eXvuIa6PX2hnj2kg0IrMLiIAIyC8DEo8mO2SjswXYbMlTAJbzPq1UmMj8uxXAnZWVleUHwFr2Q/nJrKPN4PlXxh24dizTA6sWexJAFlIz84AvLj5SAY7cUk63JA7MRFJnANvpUCiUeA6ipExF0kaU97wrKWt3jkc87zKSfqwuCSAK/Xkwnrm0dR+5CS5XK9OQ6QRsBQAzx9MvYj169KNbQPgGnB149CnkON0PUN5RFRrrkqKN9OdBy4HM1OatxcihBbDmnEwAtkN1dQ4OTaifuaxHlcW3t4ZdCx9H730GJd2ARYLdhQxnZ2Vl5S29IqTKSPhPvtRUD0qmDZiTP5oDbEdl+d7ab7KyPrhiLYNPZgHC6Q+4fwJKZHB+Gt9tCLdPZGTNvmqNNNfAfgWgqAE2hsDQ/vTU2vzj3NzcmrUMPlwCYse/hcVDkO3i4eEguw5h8EWOn4CsvO9uOQFQfcLjmVRSBh+mfv+fNvJl7bQ0Y31ntONvKvBVicfj42P7SEps4YDd43my6xDCr9ZGvDIwcb46DPCNJEkm5g3FLoDJXoBN8zmbOtFbdWEZSEiSJLXhl8q8eCRJkjwwOiwyu4AIiIAIyM+E3NRQiFNLQAREQAREQAREQLQGmfwOAN/2cnVPNAYZ7gs+GgrOnZnC50oEpr9/SHAl4nykRciw9F1ZsGhlvGnw6lD5bGXMstQ0fn5Vi5AB9OhpqDTphwa5M1rFSFPBrHYhbZMEhgaJFfego0F7kMnvHt7nGrI9Ym4cnxiH6yGsNQ3e7nXR/QqIgAiIgAjILxyy7NEgxNCNsn8Z18ln1tkLNQixFeH63XVMDTDzBFj3KtgLiS5pDbLyARWmbE6MKRk56V0MHM5Pj9oLN470JMsBr/h8Pp8v9PI2khJKC6cqbys3Slh/n5QABqs9LyN5ZizPZWdkZJSEXw7Jv/4PznUcw/Dn1NSU6FupqR802X/9h3mSSfICx07Ijb9e5tuDNVy5CAr7wyjYMwfSkmiSrM8YeZXud+5Xs4TfGmHtw0vGHCxHrxyZsBdSUaa9PDIMuOMQGXApMG1fJuBFcSqag4ghioAIiIBoCSImDIhTS0AEREAEREAERAOQxIuemK1qD2KrqnUysG3l+rfag5xNIDsyJzba2mLD3Y3LoN6sn12/6zGZHGpPU1A7kBFdT5RyGv0um+VeUIL52o0evxkapvpHgtWagair6kgV5RSZvnZY5tGDKlcP+c3cvklDlemqdmqkbMGrpzTW6PF5LYPeKpi94T/vNy+WLAf7uwNj2oGEerpC3J2M9coxS491HRi+vrQxPm21DqpDnU+0A3kmLPMkc4jMLiACIiACkoyQR0OugHYhzzwS3XisLC0Z77qtvApE/sT/I+QKic9lFPnKPD1rzI4kh8P/ifxySN/hXp6F0JvDyaruA+Fz7RRbX/9BP5FlWZbdz690pPW8DNJ3+Fms8QqMfMbCnPPjcfc/1b+E38DfL2dkZBRsH5/ukOyYwpGuZxuk4xwFha0H76kpt798IyeSvNMB+vSV/14jzvT25yGPD44lfq8oH41RdaD3zTSJFzyosDPt9svayHMS4/7Uw72gez/9oMzMbzeTpdfa4XhRr3X3iH/HjpEE4M5LFof/yN1XySM/MXu85T1v0uSR9f9niBIMk8QhBo0C8kYg88l1Y31e1IiACIiACIiA7Bpkz7wgeM+8sjm2N16irewjHgisajwCAYV/A1HeMsl2Bkj/AAAAAElFTkSuQmCC",
"description": null
},
"widgetTypes": [
{
"alias": "date_range_navigator",
"name": "Date-range-navigator",
"image": "data:image/png;base64,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",
"description": null,
"descriptor": {
"type": "static",
"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": {
"alias": "entity_admin_widgets",
"title": "Entity admin widgets",
"image": null
"image": "data:image/png;base64,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",
"description": null
},
"widgetTypes": [
{
"alias": "device_admin_table",
"name": "Device admin table",
"image": "data:image/png;base64,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",
"description": null,
"descriptor": {
"type": "latest",
"sizeX": 7.5,
@ -24,6 +27,8 @@
{
"alias": "asset_admin_table",
"name": "Asset admin table",
"image": "data:image/png;base64,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",
"description": null,
"descriptor": {
"type": "latest",
"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) {
databaseTsUpgradeService.upgradeDatabase("3.2.1");
}
databaseEntitiesUpgradeService.upgradeDatabase("3.2.1");
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
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.");
}
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:
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 alias;
private String name;
private String image;
private String description;
private transient JsonNode descriptor;
public WidgetType() {
@ -47,6 +49,8 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
this.bundleAlias = widgetType.getBundleAlias();
this.alias = widgetType.getAlias();
this.name = widgetType.getName();
this.image = widgetType.getImage();
this.description = widgetType.getDescription();
this.descriptor = widgetType.getDescriptor();
}
@ -82,6 +86,14 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
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() {
return descriptor;
}
@ -97,9 +109,10 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
sb.append(", bundleAlias='").append(bundleAlias).append('\'');
sb.append(", alias='").append(alias).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('}');
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 String alias;
private String title;
private byte[] image;
private String image;
private String description;
public WidgetsBundle() {
super();
@ -45,6 +46,7 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
this.alias = widgetsBundle.getAlias();
this.title = widgetsBundle.getTitle();
this.image = widgetsBundle.getImage();
this.description = widgetsBundle.getDescription();
}
public TenantId getTenantId() {
@ -71,14 +73,18 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
this.title = title;
}
public byte[] getImage() {
public String getImage() {
return image;
}
public void setImage(byte[] image) {
public void setImage(String image) {
this.image = image;
}
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
@Override
public String getSearchText() {
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 + (alias != null ? alias.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;
}
@ -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 (alias != null ? !alias.equals(that.alias) : that.alias != 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
@ -114,7 +123,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
sb.append("tenantId=").append(tenantId);
sb.append(", alias='").append(alias).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('}');
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_TITLE_PROPERTY = TITLE_PROPERTY;
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_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_ALIAS_PROPERTY = ALIAS_PROPERTY;
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_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)
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")
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY)
private JsonNode descriptor;
@ -71,6 +77,8 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement
this.bundleAlias = widgetType.getBundleAlias();
this.alias = widgetType.getAlias();
this.name = widgetType.getName();
this.image = widgetType.getImage();
this.description = widgetType.getDescription();
this.descriptor = widgetType.getDescriptor();
}
@ -84,6 +92,8 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement
widgetType.setBundleAlias(bundleAlias);
widgetType.setAlias(alias);
widgetType.setName(name);
widgetType.setImage(image);
widgetType.setDescription(description);
widgetType.setDescriptor(descriptor);
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)
private String searchText;
@Column(name = ModelConstants.WIDGETS_BUNDLE_IMAGE_PROPERTY)
private String image;
@Column(name = ModelConstants.WIDGETS_BUNDLE_DESCRIPTION)
private String description;
public WidgetsBundleEntity() {
super();
}
@ -62,6 +68,8 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl
}
this.alias = widgetsBundle.getAlias();
this.title = widgetsBundle.getTitle();
this.image = widgetsBundle.getImage();
this.description = widgetsBundle.getDescription();
}
@Override
@ -83,6 +91,8 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl
}
widgetsBundle.setAlias(alias);
widgetsBundle.setTitle(title);
widgetsBundle.setImage(image);
widgetsBundle.setDescription(description);
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),
descriptor varchar(1000000),
name varchar(255),
tenant_id uuid
tenant_id uuid,
image varchar(1000000),
description varchar(255)
);
CREATE TABLE IF NOT EXISTS widgets_bundle (
@ -299,7 +301,9 @@ CREATE TABLE IF NOT EXISTS widgets_bundle (
alias varchar(255),
search_text varchar(255),
tenant_id uuid,
title varchar(255)
title varchar(255),
image varchar(1000000),
description varchar(255)
);
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),
descriptor varchar(1000000),
name varchar(255),
tenant_id uuid
tenant_id uuid,
image varchar(1000000),
description varchar(255)
);
CREATE TABLE IF NOT EXISTS widgets_bundle (
@ -324,7 +326,9 @@ CREATE TABLE IF NOT EXISTS widgets_bundle (
alias varchar(255),
search_text varchar(255),
tenant_id uuid,
title varchar(255)
title varchar(255),
image varchar(1000000),
description varchar(255)
);
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,
type: widgetTypeInfo.type,
title: widgetTypeInfo.widgetName,
image: widgetTypeInfo.image,
description: widgetTypeInfo.description,
sizeX,
sizeY,
row: top,
@ -179,8 +181,9 @@ export class WidgetService {
public saveWidgetType(widgetInfo: WidgetInfo,
id: WidgetTypeId,
bundleAlias: string,
createdTime: number,
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,
defaultHttpOptionsFromConfig(config)).pipe(
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',
type: editWidgetInfo.type,
title: 'My widget',
image: null,
description: null,
sizeX: editWidgetInfo.sizeX * 2,
sizeY: editWidgetInfo.sizeY * 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,
type: widgetInfo.type,
title: widgetInfo.widgetName,
image: widgetInfo.image,
description: widgetInfo.description,
sizeX,
sizeY,
row: 0,

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

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

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

@ -18,11 +18,14 @@ import {
ChangeDetectorRef,
Component,
Inject,
Injector,
Input,
NgZone,
OnDestroy,
OnInit,
StaticProvider,
ViewChild,
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
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 { AuthUser } from '@shared/models/user.model';
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 { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service';
@ -80,7 +90,10 @@ import {
import { EntityAliases } from '@app/shared/models/alias.models';
import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget.component';
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 {
ManageDashboardLayoutsDialogComponent,
@ -99,6 +112,14 @@ import { ImportExportService } from '@home/components/import-export/import-expor
import { AuthState } from '@app/core/auth/auth.models';
import { FiltersDialogComponent, FiltersDialogData } from '@home/components/filter/filters-dialog.component';
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
@Component({
@ -146,6 +167,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
isAddingWidget = false;
isAddingWidgetClosed = true;
widgetsBundle: WidgetsBundle = null;
searchBundle = '';
widgetTypes: WidgetTypes[] = [];
filterWidgetTypes: widgetType[] = null;
isToolbarOpened = false;
isToolbarOpenedAnimate = false;
@ -260,6 +284,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private dialog: MatDialog,
private translate: TranslateService,
private ngZone: NgZone,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef) {
super(store);
@ -856,8 +882,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
}
addWidgetFromType(widget: Widget) {
addWidgetFromType(widget: WidgetInfo) {
this.onAddWidgetClosed();
this.widgetTypes = [];
this.searchBundle = '';
this.widgetComponentService.getWidgetInfo(widget.bundleAlias, widget.typeAlias, widget.isSystemType).subscribe(
(widgetTypeInfo) => {
const config: WidgetConfig = JSON.parse(widgetTypeInfo.defaultConfig);
@ -869,6 +897,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.type,
title: 'New widget',
image: null,
description: null,
sizeX: widgetTypeInfo.sizeX,
sizeY: widgetTypeInfo.sizeY,
config,
@ -1115,4 +1145,62 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
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.
-->
<div>
<mat-tab-group *ngIf="hasWidgetTypes()" class="tb-absolute-fill" fxFlex>
<mat-tab *ngIf="timeseriesWidgetTypes.length" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="timeseriesWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
<mat-tab *ngIf="latestWidgetTypes.length" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="latestWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
<mat-tab *ngIf="rpcWidgetTypes.length" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="rpcWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[isMobile]="true"
[disableWidgetInteraction]="true"
[isEditActionEnabled]="false"
[isExportActionEnabled]="false"
[isRemoveActionEnabled]="false"
[callbacks]="callbacks"></tb-dashboard>
</mat-tab>
<mat-tab *ngIf="alarmWidgetTypes.length" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">
<tb-dashboard [aliasController]="aliasController"
[widgets]="alarmWidgetTypes"
[widgetLayouts]="{}"
[isEdit]="false"
[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 class="widget-select">
<div *ngIf="widgetsBundle; else bundles">
<div *ngIf="(widgets$ | async)?.length; else emptyBundle" fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
<div *ngFor="let widget of widgets$ | async" class="mat-card-container">
<mat-card fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="onWidgetClicked($event, widget)">
<div class="preview-container" fxFlex="45">
<img class="preview" [src]="getPreviewImage(widget.image)" alt="{{ widget.title }}">
</div>
<div fxFlex fxLayout="column">
<mat-card-title>{{widget.title}}</mat-card-title>
<mat-card-subtitle>{{ 'widget.' + widget.type | translate }}</mat-card-subtitle>
<mat-card-content *ngIf="widget.description">
{{ widget.description }}
</mat-card-content>
</div>
</mat-card>
</div>
</div>
<ng-template #emptyBundle>
<span translate
style="display: flex;"
fxLayoutAlign="center center"
class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
</ng-template>
</div>
<ng-template #bundles>
<div fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
<div *ngFor="let widgetsBundle of widgetsBundles$ | async" class="mat-card-container">
<mat-card fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="selectBundle($event, widgetsBundle)">
<div class="preview-container" fxFlex="45">
<img class="preview" [src]=getPreviewImage(widgetsBundle.image) alt="{{ widgetsBundle.title }}">
</div>
<div fxFlex fxLayout="column">
<mat-card-title>{{ widgetsBundle.title }}</mat-card-title>
<mat-card-subtitle *ngIf="isSystem(widgetsBundle)" translate>widgets-bundle.system</mat-card-subtitle>
<mat-card-content *ngIf="widgetsBundle.description">
{{ widgetsBundle.description }}
</mat-card-content>
</div>
</mat-card>
</div>
</div>
</ng-template>
</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
* limitations under the License.
*/
:host ::ng-deep {
.mat-tab-group {
.mat-tab-body-wrapper {
height: 100%;
@import '../../../../../scss/constants';
:host{
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.
///
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 { IAliasController } from '@core/api/widget-api.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
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 { 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({
selector: 'tb-dashboard-widget-select',
templateUrl: './dashboard-widget-select.component.html',
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()
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()
aliasController: IAliasController;
@Input()
set searchBundle(search: string) {
this.search$.next(search);
}
@Input()
set filterWidgetTypes(widgetTypes: Array<widgetType>) {
this.filterWidgetTypes$.next(widgetTypes);
}
@Output()
widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>();
widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>();
timeseriesWidgetTypes: Array<Widget> = [];
latestWidgetTypes: Array<Widget> = [];
rpcWidgetTypes: Array<Widget> = [];
alarmWidgetTypes: Array<Widget> = [];
staticWidgetTypes: Array<Widget> = [];
@Output()
widgetsBundleSelected: EventEmitter<WidgetsBundle> = new EventEmitter<WidgetsBundle>();
callbacks: DashboardCallbacks = {
onWidgetClicked: this.onWidgetClicked.bind(this)
};
@Output()
widgetsTypes: EventEmitter<Set<widgetType>> = new EventEmitter<Set<widgetType>>();
constructor(private widgetsService: WidgetService) {
constructor(private widgetsService: WidgetService,
private sanitizer: DomSanitizer) {
}
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 {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (change.currentValue !== change.previousValue && change.currentValue) {
if (propName === 'widgetsBundle') {
this.loadLibrary();
}
private getWidgets(): Observable<Array<WidgetInfo>> {
if (!this.widgetsInfo) {
if (this.widgetsBundle !== null) {
const bundleAlias = this.widgetsBundle.alias;
const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
this.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() {
this.timeseriesWidgetTypes.length = 0;
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;
});
}
);
onWidgetClicked($event: Event, widget: WidgetInfo): void {
this.widgetSelected.emit(widget);
}
hasWidgetTypes() {
return this.timeseriesWidgetTypes.length > 0 ||
this.latestWidgetTypes.length > 0 ||
this.rpcWidgetTypes.length > 0 ||
this.alarmWidgetTypes.length > 0 ||
this.staticWidgetTypes.length > 0;
isSystem(item: WidgetsBundle): boolean {
return item && item.tenantId.id === NULL_UUID;
}
private onWidgetClicked($event: Event, widget: Widget, index: number): void {
this.widgetSelected.emit(widget);
selectBundle($event: Event, bundle: WidgetsBundle) {
$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>
<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 tb-details-title-header" fxFlex fxLayout="column" fxLayoutAlign="start start">
<span class="tb-details-title">{{ headerTitle }}</span>
<div class="mat-toolbar-tools tb-details-title-header" fxFlex fxLayout="column" fxLayoutAlign="start start" [fxHide]="showSearchPane">
<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 style="width: 100%;">
<ng-content select=".header-pane"></ng-content>
</span>
</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>
<button mat-button mat-icon-button (click)="onCloseDetails()">
<mat-icon class="material-icons">close</mat-icon>
@ -51,6 +65,6 @@
</section>
</mat-toolbar>
</header>
<div fxFlex class="mat-content">
<div fxFlex class="mat-content" [ngStyle]="{backgroundColor: backgroundColor}">
<ng-content></ng-content>
</div>

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

@ -28,7 +28,7 @@
:host ::ng-deep {
.mat-toolbar-tools {
height: 100%;
min-height: 100px;
min-height: 60px;
max-height: 120px;
&.tb-details-title-header {
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() isReadOnly = false;
@Input() isAlwaysEdit = false;
@Input() isShowSearch = false;
@Input() backgroundColor = '#FFF';
theFormValue: FormGroup;
@ -50,8 +52,11 @@ export class DetailsPanelComponent extends PageComponent {
toggleDetailsEditMode = new EventEmitter<boolean>();
@Output()
applyDetails = new EventEmitter<void>();
@Output()
closeSearch = new EventEmitter<void>();
isEditValue = false;
showSearchPane = false;
@Output()
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,
Input,
OnDestroy,
OnInit,
Output,
QueryList,
ViewChild,
@ -52,7 +51,7 @@ import { deepClone, mergeDeep } from '@core/utils';
styleUrls: ['./entity-details-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EntityDetailsPanelComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
export class EntityDetailsPanelComponent extends PageComponent implements AfterViewInit, OnDestroy {
@Output()
closeEntityDetails = new EventEmitter<void>();
@ -140,10 +139,6 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
return this.isEditValue;
}
ngOnInit(): void {
this.init();
}
private init() {
this.translations = this.entitiesTableConfig.entityTranslations;
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 { 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 { DisplayWidgetTypesPanelComponent } from '@home/components/dashboard-page/widget-types-panel.component';
@NgModule({
declarations:
@ -244,7 +245,8 @@ import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/emb
DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent,
EmbedDashboardDialogComponent
EmbedDashboardDialogComponent,
DisplayWidgetTypesPanelComponent
],
imports: [
CommonModule,
@ -345,7 +347,8 @@ import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/emb
DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent,
EmbedDashboardDialogComponent
EmbedDashboardDialogComponent,
DisplayWidgetTypesPanelComponent
],
providers: [
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;
this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe(
(widgetTypes) => {
widgetTypes = widgetTypes.sort((a, b) => a.createdTime - b.createdTime);
const widgetsBundleItem: WidgetsBundleItem = {
widgetsBundle: this.prepareExport(widgetsBundle),
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,
settingsSchema: this.utils.editWidgetInfo.settingsSchema,
dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
image: this.utils.editWidgetInfo.image,
description: this.utils.editWidgetInfo.description,
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();

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

@ -347,6 +347,8 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
alias: string;
typeSettingsSchema?: string | any;
typeDataKeySettingsSchema?: string | any;
image: string;
description: string;
componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
}
@ -375,6 +377,8 @@ export const MissingWidgetType: WidgetInfo = {
controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n',
image: null,
description: null,
defaultConfig: '{\n' +
'"title": "Widget type not found",\n' +
'"datasources": [],\n' +
@ -398,6 +402,8 @@ export const ErrorWidgetType: WidgetInfo = {
controllerScript: 'self.onInit = function() {}',
settingsSchema: '{}\n',
dataKeySettingsSchema: '{}\n',
image: null,
description: null,
defaultConfig: '{\n' +
'"title": "Widget failed to load",\n' +
'"datasources": [],\n' +
@ -424,6 +430,8 @@ export interface WidgetTypeInstance {
export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
return {
widgetName: widgetTypeEntity.name,
image: widgetTypeEntity.image,
description: widgetTypeEntity.description,
alias: widgetTypeEntity.alias,
type: widgetTypeEntity.descriptor.type,
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 = {
type: widgetInfo.type,
sizeX: widgetInfo.sizeX,
@ -454,9 +463,12 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
return {
id,
tenantId,
createdTime,
bundleAlias,
alias: widgetInfo.alias,
name: widgetInfo.widgetName,
image: widgetInfo.image,
description: widgetInfo.description,
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 #topLeftPanel class="tb-split tb-content">
<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="mat-padding">
<div fxFlex fxLayout="row" style="max-height: 40px;"
@ -153,7 +153,7 @@
</div>
</div>
</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-editor-area-title-panel">
<button mat-button (click)="beautifyHtml()">
@ -169,7 +169,7 @@
<div #htmlInput></div>
</div>
</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-editor-area-title-panel">
<button mat-button (click)="beautifyCss()">
@ -189,7 +189,7 @@
</div>
<div #topRightPanel class="tb-split tb-content">
<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-editor-area-title-panel">
<button mat-button (click)="beautifyJson()">
@ -205,7 +205,7 @@
<div #settingsJsonInput></div>
</div>
</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-editor-area-title-panel">
<button mat-button (click)="beautifyDataKeyJson()">
@ -221,6 +221,25 @@
<div #dataKeySettingsJsonInput></div>
</div>
</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>
</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() {
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) => {
this.setWidgetType(widgetTypeInstance);
this.saveWidgetPending = false;
@ -543,7 +544,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
config.title = this.widget.widgetName;
this.widget.defaultConfig = JSON.stringify(config);
this.isDirty = false;
this.widgetService.saveWidgetType(this.widget, undefined, saveWidgetAsData.bundleAlias).subscribe(
this.widgetService.saveWidgetType(this.widget, undefined, saveWidgetAsData.bundleAlias, undefined).subscribe(
(widgetTypeInstance) => {
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 }}
</mat-error>
</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>
</form>
</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 {
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) {
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>
</ng-container>
<div class="tb-hint" *ngIf="maxSizeByte" translate [translateParams]="{ size: maxSizeByte | fileSize}">dashboard.maximum-upload-file-size</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 { DomSanitizer, SafeUrl } from '@angular/platform-browser';
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({
selector: 'tb-image-input',
@ -42,6 +45,9 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
@Input()
label: string;
@Input()
maxSizeByte: number;
private requiredValue: boolean;
get required(): boolean {
@ -80,7 +86,10 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
constructor(protected store: Store<AppState>,
private utils: UtilsService,
private sanitizer: DomSanitizer) {
private sanitizer: DomSanitizer,
private dialog: DialogService,
private translate: TranslateService,
private fileSize: FileSizePipe) {
super(store);
}
@ -88,6 +97,15 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
this.autoUploadSubscription = this.flow.events$.subscribe(event => {
if (event.type === 'fileAdded') {
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();
reader.onload = (loadEvent) => {
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 {
color: #fff;
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,
{
name: 'widget.latest-values',
name: 'widget.latest',
icon: 'track_changes',
configHelpLinkId: 'widgetsConfigLatest',
template: {
@ -170,6 +170,8 @@ export interface WidgetType extends BaseData<WidgetTypeId> {
bundleAlias: string;
alias: string;
name: string;
image: string;
description: string;
descriptor: WidgetTypeDescriptor;
}
@ -399,14 +401,8 @@ export interface WidgetConfig {
[key: string]: any;
}
export interface Widget {
id?: string;
export interface Widget extends WidgetInfo{
typeId?: WidgetTypeId;
isSystemType: boolean;
bundleAlias: string;
typeAlias: string;
type: widgetType;
title: string;
sizeX: number;
sizeY: number;
row: number;
@ -414,6 +410,17 @@ export interface Widget {
config: WidgetConfig;
}
export interface WidgetInfo {
id?: string;
isSystemType: boolean;
bundleAlias: string;
typeAlias: string;
type: widgetType;
title: string;
image: string;
description: string;
}
export interface GroupInfo {
formIndex: number;
GroupTitle: string;
@ -429,7 +436,7 @@ export interface JsonSchema {
export interface JsonSettingsSchema {
schema?: JsonSchema;
form?: any[];
groupInfoes?: GroupInfo[]
groupInfoes?: GroupInfo[];
}
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;
title: 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 './nospace.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 { ContactComponent } from '@shared/components/contact.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({
providers: [
@ -146,6 +148,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
HighlightPipe,
TruncatePipe,
TbJsonPipe,
FileSizePipe,
{
provide: FlowInjectionToken,
useValue: Flow
@ -219,12 +222,14 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
HighlightPipe,
TruncatePipe,
TbJsonPipe,
FileSizePipe,
KeyboardShortcutPipe,
TbJsonToStringDirective,
JsonObjectEditDialogComponent,
HistorySelectorComponent,
EntityGatewaySelectComponent,
ContactComponent
ContactComponent,
WidgetsBundleSearchComponent
],
imports: [
CommonModule,
@ -387,11 +392,13 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
TruncatePipe,
TbJsonPipe,
KeyboardShortcutPipe,
FileSizePipe,
TranslateModule,
JsonObjectEditDialogComponent,
HistorySelectorComponent,
EntityGatewaySelectComponent,
ContactComponent
ContactComponent,
WidgetsBundleSearchComponent
]
})
export class SharedModule { }

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

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

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

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

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

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

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

@ -646,6 +646,7 @@
"add-widget": "Add new widget",
"title": "Title",
"select-widget-title": "Select widget",
"select-widget-value": "{{title}}: select widget",
"select-widget-subtitle": "List of available widget types",
"delete": "Delete dashboard",
"title-required": "Title is required.",
@ -691,6 +692,8 @@
"background-size-mode": "Background size mode",
"no-image": "No image selected",
"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",
"columns-count": "Columns count",
"columns-count-required": "Columns count is required.",
@ -2205,6 +2208,7 @@
"widget": {
"widget-library": "Widgets Library",
"widget-bundle": "Widgets Bundle",
"all-bundles": "All bundles",
"select-widgets-bundle": "Select widgets bundle",
"management": "Widget management",
"editor": "Widget Editor",
@ -2217,7 +2221,7 @@
"timeseries": "Time series",
"search-data": "Search data",
"no-data-found": "No data found",
"latest-values": "Latest values",
"latest": "Latest values",
"rpc": "Control widget",
"alarm": "Alarm widget",
"static": "Static widget",
@ -2244,6 +2248,9 @@
"css": "CSS",
"settings-schema": "Settings schema",
"datakey-settings-schema": "Data key settings schema",
"widget-settings": "Widget settings",
"description": "Description",
"image-preview": "Image preview",
"javascript": "Javascript",
"js": "JS",
"remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?",
@ -2257,7 +2264,9 @@
"export": "Export widget",
"no-data": "No data to display on widget",
"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": {
"header-button": "Widget header button",
@ -2286,6 +2295,8 @@
"delete": "Delete widgets bundle",
"title": "Title",
"title-required": "Title is required.",
"description": "Description",
"image-preview": "Image preview",
"add-widgets-bundle-text": "Add new widgets bundle",
"no-widgets-bundles-text": "No widgets bundles found",
"empty": "Widgets bundle is empty",

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

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

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

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

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

@ -1494,7 +1494,7 @@
"export": "Exporter widget",
"html": "HTML",
"javascript": "Javascript",
"latest-values": "Dernières valeurs",
"latest": "Dernières valeurs",
"management": "Gestion des widgets",
"missing-widget-title-error": "Le titre du widget doit être spécifié!",
"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",
"search-data": "Cerca dati",
"no-data-found": "Nessun dato trovato",
"latest-values": "Ultimi valori",
"latest": "Ultimi valori",
"rpc": "Control widget",
"alarm": "Alarm widget",
"static": "Static widget",

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

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

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

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

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

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

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

@ -1468,7 +1468,7 @@
"timeseries": "Laika sērijas",
"search-data": "Meklēt datus",
"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",
"alarm": "Trauksmes 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",
"search-data": "Pesquisar dados",
"no-data-found": "Nenhum dado encontrado",
"latest-values": "Últimos valores",
"latest": "Últimos valores",
"rpc": "Widget de controle",
"alarm": "Widget de alarme",
"static": "Widget estático",

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1396,7 +1396,7 @@
"timeseries": "時間序列",
"search-data": "搜尋資料",
"no-data-found": "沒有找到資料",
"latest-values": "最新值",
"latest": "最新值",
"rpc": "控件部件",
"alarm": "警告部件",
"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 {
@include tb-mat-icon-size(20);
}
&.tb-mat-28 {
@include tb-mat-icon-size(28);
}
&.tb-mat-32 {
@include tb-mat-icon-size(32);
}
@ -762,6 +765,9 @@ mat-label {
}
.mat-icon-button {
&.tb-mat-28 {
@include tb-mat-icon-button-size(28);
}
&.tb-mat-32 {
@include tb-mat-icon-button-size(32);
}

Loading…
Cancel
Save