Browse Source

Merge branch 'vvlladd28-feature/dashboard/widget-select-preview'

pull/4218/head
Igor Kulikov 6 years ago
parent
commit
9187a827e7
  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. 11
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  16. 52
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  17. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  18. 7
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  19. 11
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  20. 51
      application/src/test/java/org/thingsboard/server/controller/BaseWidgetTypeControllerTest.java
  21. 10
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  22. 49
      common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java
  23. 75
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java
  24. 45
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
  25. 46
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java
  26. 22
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
  27. 3
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  28. 86
      dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java
  29. 69
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java
  30. 43
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeEntity.java
  31. 56
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeInfoEntity.java
  32. 10
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetsBundleEntity.java
  33. 29
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  34. 29
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  35. 35
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
  36. 75
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  37. 8
      dao/src/main/resources/sql/schema-entities-hsql.sql
  38. 8
      dao/src/main/resources/sql/schema-entities.sql
  39. 43
      dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java
  40. 51
      ui-ngx/src/app/core/http/widget.service.ts
  41. 2
      ui-ngx/src/app/modules/dashboard/dashboard-pages.routing.module.ts
  42. 46
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  43. 94
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  44. 114
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html
  45. 58
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss
  46. 204
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  47. 23
      ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html
  48. 36
      ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.scss
  49. 48
      ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.ts
  50. 20
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  51. 2
      ui-ngx/src/app/modules/home/components/details-panel.component.scss
  52. 11
      ui-ngx/src/app/modules/home/components/details-panel.component.ts
  53. 7
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts
  54. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  55. 4
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  56. 55
      ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts
  57. 2
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  58. 29
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  59. 29
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  60. 40
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.ts
  61. 12
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  62. 10
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.html
  63. 10
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle.component.ts
  64. 1
      ui-ngx/src/app/shared/components/image-input.component.html
  65. 4
      ui-ngx/src/app/shared/components/image-input.component.scss
  66. 20
      ui-ngx/src/app/shared/components/image-input.component.ts
  67. 29
      ui-ngx/src/app/shared/components/widgets-bundle-search.component.html
  68. 55
      ui-ngx/src/app/shared/components/widgets-bundle-search.component.scss
  69. 74
      ui-ngx/src/app/shared/components/widgets-bundle-search.component.ts
  70. 1
      ui-ngx/src/app/shared/components/widgets-bundle-select.component.scss
  71. 39
      ui-ngx/src/app/shared/models/widget.models.ts
  72. 1
      ui-ngx/src/app/shared/models/widgets-bundle.model.ts
  73. 56
      ui-ngx/src/app/shared/pipe/file-size.pipe.ts
  74. 1
      ui-ngx/src/app/shared/pipe/public-api.ts
  75. 11
      ui-ngx/src/app/shared/shared.module.ts
  76. 2
      ui-ngx/src/assets/locale/locale.constant-cs_CZ.json
  77. 2
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  78. 2
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  79. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  80. 2
      ui-ngx/src/assets/locale/locale.constant-es_ES.json
  81. 2
      ui-ngx/src/assets/locale/locale.constant-fa_IR.json
  82. 2
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  83. 2
      ui-ngx/src/assets/locale/locale.constant-it_IT.json
  84. 2
      ui-ngx/src/assets/locale/locale.constant-ja_JA.json
  85. 2
      ui-ngx/src/assets/locale/locale.constant-ka_GE.json
  86. 2
      ui-ngx/src/assets/locale/locale.constant-ko_KR.json
  87. 2
      ui-ngx/src/assets/locale/locale.constant-lv_LV.json
  88. 2
      ui-ngx/src/assets/locale/locale.constant-pt_BR.json
  89. 2
      ui-ngx/src/assets/locale/locale.constant-ro_RO.json
  90. 2
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  91. 2
      ui-ngx/src/assets/locale/locale.constant-sl_SI.json
  92. 2
      ui-ngx/src/assets/locale/locale.constant-tr_TR.json
  93. 2
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json
  94. 4
      ui-ngx/src/assets/locale/locale.constant-zh_CN.json
  95. 2
      ui-ngx/src/assets/locale/locale.constant-zh_TW.json
  96. 1
      ui-ngx/src/assets/widget-preview-empty.svg
  97. 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,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAfbSURBVHja7d3tUxNJHsBx/jLcs+64rWVx9di9xYdSuUWL3boVo0b3BDwJCuFZkVWEEy8adYVEEZWFE42gPAgXNAQisCIPMUAYCI95fvjeC/R8AC2oYg2D3W8mM91JzSfdv+meVE86Av/Y0IDM09CYjwi/dTqAzFNg2uqPGJtmDaTpsYihwFqABIYiBlgTaUBABERABERABERA1jbEJ0n+95Qen9+0+mQB6U4vz2uYf+mvfDvrxrww1ykPiJZgjoSp3EjVwRb6ytsAf0Nd5Rz3QsH68n5yneP3Ga5oCNmbrwVWNYTbzdaKiXzrSPq0vWD8Yge49/e1/MyxYM2tseyZ3OEsuzNrtOq+RdUTWt2Q6qZQwxW1yavm/kl9iR7c2aDiWLDAxZw/N6WNDrX+QqlFt8qbFvnDzZX+6yavmvoqSZoDdzahoxwLFk4y6s5tzvSYL0rS9CqHZFYX3saUV3u4JZTSNZvZcLkP3HvvXNZzLPik+H6BP9fZfs6Xc+96++qGOC29M8Bg98Q4tn5c5lHAnT3UHeJ5CHunh4EAfT5fp5U5u+w6RHf2GunZQ24xRBEQAREQARGQ3wXSObVqIV35JgBLZsr8/ouULUkd0Htg8+EXABxJSEhIuPWydHzrq/dNHN/6fSPMZW7d2w3PDm5W9gK9p5rCBNmeuKEJaNpYOZ8X2FLpvfuFy7OxwVueOF/ips1mm10A2VPqMUaNkpPlrN/kd/3lrvdabIgfvvv61zBBrCQ0QfBb46s79Z+APw9O6WBsfQhgeyMAwwXp9RBfmZdpAQjsd8H2NjYMwLaOwWPgiZxmiP2/hi1GEppgOOb0LtX4qyPmbwLgcZw6wmvIZGyVcetD4uMf6j8fe1luJHo2sM4JSXcBHn4HrBjkN4PBYDAYhpYJaV93U8rfy31DEzC71QhoY7/onodERUdv40o6VKcQ3wypV1/+ApNYiyvSBQdqAOmb3pWEeIuVSqXynH+ZkMZdMLvOU1J8ETw/auczHn3le10jeV/GxcWlEN8KxacACB4pAP+6WUgywFz8bVYSgrdYqSzzL7dp9W0K4vzMCxBILgLGqiC0fuw1pCz3/8GePg8tOBoENj2DzWa8e17qVy5GvMWawLJjJJR43lGkBAhlH7DZbFNTMQ1z17e9EexD0c1Tuiri9w22fT4I8O+EQZttgpNpkzWxgUBKms1mm1lRCN7A8oI9rxNwpO9UTwL4FQqFQlFB18HtaTYAsjoB6FBuz5wk50ZSogGAVIVCoSjBfeJvh/qZVCgUCkU1UNwmhigCIiACIiCfEOSaLJJoWgIiIAIiIAIiIAIiIAKyEhD/zPuL+x/JCDJUOr819Sws7syRF8TQUT5oyz9lD97T27nXaqqDulC/3hCUGyTtYb/KU1nru24YVgcz7kyW2CfOeM5JVxrlBlGBijtNpOv0qVKGH1N1XTumK6dvyhVyfFiS/Bl+AieL/LYzbqM8IS1nHY1nH2pCGX64qkVS3VGXywrifkEf9BE0zzLc6aI/BNNTIJmnbMEBGUFEzy4gAiIgAiIgAvKBNNIvM0j5JRhJ9EFm81vHtWqZQQyJULnOSCDqhbwhjvVufjpwhqexYMnI6gT/pSMaL1o1UwVWOcXItnZfTE88FWk8jW2o3/CCfx43Z6ajVTt3nQ/nuS57vlaOpk1JnHSkEtVlKNSMRPlw/sGvVf29KKxf+rLna/1HWaQnvzq2n8SNcXFxpcb1cXFxcZL2sz/WEHbJcuZr2b/aOUzj/pgQyZUAAzEBAG2S5cvBcEuWN1/r2x3g+lMKPPhrj3TaFEw8M/UkM6RVo98R5sdIljlf64IeKLwL3Nmz86wPR9aOfSbqfoHCajFEERABERABEZAPQ8R8LdG0BERABERABERABERAPmmI3b74fx35puUFOX9Jn+exNi/MsLXICuI9CvVPSjJ+o63iGcbHOgtAz9UHIbtZ0uv1A7To+mVRI2XFPaFAs8bV+ouUO6W5YTtuB6dq9HJnl84z0JMy3qiXsmdlESPPNXluSwVlGn3WE80AdY0QLLzcS5cO/mXi7EX9/BPtqxwy2gj6dksFZU8kyaMZ4HYruB2jxW1dOup1UNwlSV4ZQHy5t+szJqxq67O8xnNOTckDlRNmsut/NnXpRvcbGvq7CxrPueXQtIL9HU7olXCYp9F0dcwAzJmHmbPPWSyWMcbNM/LrEDWrswddPsQbFEMUAREQAREQAfk0IaUJuw9Xh2QJcdS+uacqsj36IWO1nHKtY+kQR3LS+JsQDThjOglUphUMc+kxXGsLm2M8KdmxVIgjOek570A4epG81KearVxIIRD9/Hc/4WCTwWAwGO4vuD8YVPxDWhpk4h3HPOREEf1O10Tk9EiUy7jzY7T6VKVSeei/CzMWlSwCcR1OfOf7VmmAVC2P4/elRk7yY90JLR9HspgDnicedi2lRmp2lwYXQKajLWwxEoicpCr565GPEg7Wo8ZFG9353TeXFiM1u0vekqjyegzxebDtkvVU5AizUd9/pMB2LeooW8TxnqtW7dsSrUKRdi8EPYoEXUovKPVhvPoGy3bfWno/Un3I8f6Pmot1hBHiOFS9nJ79A4sO1G05E9YO0blSYy2bWQwaBURA1iakLfyzs9pEjQiIgAiIgAiIgHxikDWzQPDaWLJ5aizCtzYW0Q5ErI1lzQP8DwJX9hDY3Q8ZAAAAAElFTkSuQmCC",
"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,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAmQSURBVHja7d39V1NHGsBx/7Lgafd47FH2BhAEDIFoyIL1pb5gC4upcqwW0aJUDNpSGmFXAxVQV+silUUsqQVsFvAFRF6EKEFISSAvJDf3uz8ELOtaCZCzKp35AThzZ+7cz5lnbubkCbmrCI49GXzHy5OxAKuCwy6Zd7zIruHgqjEXK6C4xlY9kVcCRH6yapAVUQYFREAEREAEREDeGsiMbWpR7bsd4d9BmyvKkOajR4+e7VBe0bC8NoKzjUrdL9WUXXlde03d7E5JalvENdvtC0Mqky9aipIPzfxvw5obS4Kcb3ppzgxty4aYTBFAMgBHWnl4I/aK9sG5Tdr8i3tRNwcJACH5Fd3wSy3z+mjqwkdmIXKUIdQm+cC2Q9JVK+60OxDKrqPgDHhLEuNyHoFSm6beYQv3CFVqpM03gP6d0haL1M2Vj6rTpT3204lx+U7Ir8CfemW3pGkMz2uqlLQXrFuljaUB0Hz5cXzKxVnIqDEu9YQ/qpBe6QH98RbHrQ2tFByDh+pn5JbAoexux8k0F9+l3nF8lRJenxc0tsk6dR/BzE96H+VL3dRK5SO29KSSYVt6Kewpwydl3Bn5coMLYGpIuvKcXnXdZGeKBTSpt0fOS7/gktrw6j+339OXRxUyIVkpygNOGmlOnqFiH+SWMCLdheC29mBqHYTSw8v43kNQkq9wW+2AEamb2lQFvt4ow5nds5BGcEhdv4XW01ag6FPQXADyDuOS2ria4ocbScrr17nJZDLt328ymUz2CCDD0l0MO0pLS/ca8CdbybwGuSX8KIVvrk+kQ6Wlpelfhbt0nS4wJtRi0c0u9to04LweOJc9C7n923IOrxFH5WGjLm92sX+7HZfUxgltaWnpIWkimhCr9IwtH5vNZrMFTh5/HO+C3BJapXAE90snzGaz+UcAvt948a4tpZaqrMghT1JK7tgOzEEqs3BJbRzLNJvNZrMriqGlHNgN+4/OVv07peIQkFvCgPQYaBzxq+fdU3eYgbRaGjd4IoZUbgdO5oGmFjhsxCW1cX5LKLq3X1/P0cReuKFuJXS+HkKbE1rCkNCHB2doVj/hcKadqc97AdjzV3+wRqpmOvW0HDyzICQUXw+WFDu21H2gyXZgi2vEJbUxGP9tCOsXSpQgkiSpDwwAijkuXWN4DJhTZsIQhrITdUmN4MpX6xMLvOFdhmZD4sHMUmhJTNz4xYIQiuI+YmqXOjnzYAZoDqdskg6Hwkdvpabokm9FaUamRkcdgdm/PQ8ehQD8EwATLiDU2+0B4Hnn6FwX770RxekEXJ1jyugM0w5gagyYeg4TLkKjfsI/AEIPBiDU1xvwjCo4fFOdA8wdDfTd80driyJ2vwIiIAIiIAIiIAIiIAIiIAIiIAIiIAIiIAIiIAIiICsOoti94LMv8VwTdrvdbve9DRBZtQ9aVUv8lOP+dWti1q2zvh0Q1S1aVTKBniGYdD7vkX33fwXsDwKRnO37D2DCiWL3jXke2QFv1/M3BNmZ4GlVyfbkTPVJTiVo123Xpq59SllsmnY6Qsi3qTyIGdUlad+rZkC9+YPrbwbSrD/RqpLv/YObf1JOZSntql4l4dJgzJCSWRMhxB4zUL4V3RkurQ19XMgP8W8GcrvnvXKVPJmfnqqST+XQq5okw/JDTHLymuIIIWSd21KLzsKAanxDbHKCyvtmIJx6XyWXfRjqmAe5u/qZ2+mLFPJd8monOgttMTMGk9vtVN4QxJuokqvXfZ2hmnwBkQ3Z5oy2SCHO1btBt6Ei7VMa1pwp3PsmQitUNQL3qhT5ytnOqomOJpxVfq4+wFd7OpJ0K/3VANqroDN9Y/FD51ffOd/VV3Znxdop0Fne+S3K45w24HSr2GsJiIAIiIAIiIAIiIAIiIAIyIKQoRVRRGgJiIAIiIAIiIAIiID8YSG2c6/4r6uh4uLi4qF3CmLLMhS9JOkxvCg9C0OUQJSuZHnfS/R8PM9gOPdS3cUX5flrIZdSwKLN3OcH2N+mrAd7Rr81Qa/LebrAuO5Yvebg+Pya5uPLcXRkN4/nZdleqrUWzxbra0PLkRHL8KYgB64C5FiVGJwaKy25UJO7wMCu9XBNJ4N3fiJXdgJ4/It3ZBn+0jz+soPLc5F1+bWQT6yxTPZB4fU5iDfzKrTkQm96BBB2dFChzzzri/dz1tJQSEdajn6MM4Yt5sU7DIat4ywNcuOYJxbgsd43B9mjV6Alu71l298jgRy/1JMphzYPH7mFdryhUEke5NrZzq2KnOZYZFwZDIasn1ka5FfdtCcWmNA9ZhaiMu2wQEtK2d4CIoF8du1iwq5d6tsdBQO7aSh0qQH+lrhr15/bo+KIDFKXpt+8OgtPVjt0/wI7O5QYJhK6aMnFFT8WASSQNHj9iNvtDoa0Z6/TUCivD+F31h93u91yVByRrhE8sQR3lvX0DLSnP/4pzqXEwN2kyZZcqDqyEGRtj3XvSZyJbcPFbsrWe2goxFg1dqR+LLFj6LgnKo6IITOncBYVFRVV0Gg80otyDLjW+KgO/CcW+FSJt6iopBUYOJrfBMM1cP863nLjZegrzL+9yHWe3cGyIG9DOfb78/HOQX5vPqBxDtL49kNqiot/14HvXPiFvdIntvECIiACIiACIiACIiACsmiIyLOL0BIQAREQAREQAREQAXmnISLPfmcahupvht8fnrx63QNg/yWyYYNNTa3//QXKo13LcowvPc+u7ee6tvqMdgpwbqquzAiCvHlnZOO61lSdSrk5v+bhteU4OrYuPc+u7Q/EPoPj1UBXPWT1QVV+pJD1MB43iedSvdt7Awa6Rmwot853w1T9Jc+iHdnLyLNr+3sygEA4tkL3N3kY2tq5CAif/hDQX6jRyRoHBS0NhZiMNzNbfbqaC4bQoh3LyLNr++9uD0/g5SbIi/+G0M7ersVATtT9K9/t3ttlrg5sDDYUyut9DDT/s8Dt3ta7eMeS8+xo+4e0gNNhvdwEyPr7dduazmnbI4fkNddojEZj99Ot1uM0FLoSAKq0RqOxNyqOiCFy4iB8Vg9YayDv5+aqqi+Sv48YMqCetuaAB3bs7aShkDgXfT825YMnOo6IIfykKTu0fQZwpplOZAeAiEPr/aL9qR2Ecg6WbXZzeaNCQyFXsiv0D+Wdn5Xp/6959i4v/GrtDq9L388dMsB0T2TDy+3tfTIQ6mr1gLcfJgZhxDoJcmerd5EOkWd/uyAizy628QIiIAIiIALyB4esmAcEr4xHNk+OrQqsjIdoy6tWxmPNZf4DJqTD+Gup8cgAAAAASUVORK5CYII=",
"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);

11
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -78,6 +78,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
@ -584,13 +585,13 @@ public abstract class BaseController {
}
}
WidgetType checkWidgetTypeId(WidgetTypeId widgetTypeId, Operation operation) throws ThingsboardException {
WidgetTypeDetails checkWidgetTypeId(WidgetTypeId widgetTypeId, Operation operation) throws ThingsboardException {
try {
validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
WidgetType widgetType = widgetTypeService.findWidgetTypeById(getCurrentUser().getTenantId(), widgetTypeId);
checkNotNull(widgetType);
accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, operation, widgetTypeId, widgetType);
return widgetType;
WidgetTypeDetails widgetTypeDetails = widgetTypeService.findWidgetTypeDetailsById(getCurrentUser().getTenantId(), widgetTypeId);
checkNotNull(widgetTypeDetails);
accessControlService.checkPermission(getCurrentUser(), Resource.WIDGET_TYPE, operation, widgetTypeId, widgetTypeDetails);
return widgetTypeDetails;
} catch (Exception e) {
throw handleException(e, false);
}

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

@ -30,6 +30,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
@ -45,7 +47,7 @@ public class WidgetTypeController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetType/{widgetTypeId}", method = RequestMethod.GET)
@ResponseBody
public WidgetType getWidgetTypeById(@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException {
public WidgetTypeDetails getWidgetTypeById(@PathVariable("widgetTypeId") String strWidgetTypeId) throws ThingsboardException {
checkParameter("widgetTypeId", strWidgetTypeId);
try {
WidgetTypeId widgetTypeId = new WidgetTypeId(toUUID(strWidgetTypeId));
@ -58,17 +60,17 @@ public class WidgetTypeController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetType", method = RequestMethod.POST)
@ResponseBody
public WidgetType saveWidgetType(@RequestBody WidgetType widgetType) throws ThingsboardException {
public WidgetTypeDetails saveWidgetType(@RequestBody WidgetTypeDetails widgetTypeDetails) throws ThingsboardException {
try {
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
widgetType.setTenantId(TenantId.SYS_TENANT_ID);
widgetTypeDetails.setTenantId(TenantId.SYS_TENANT_ID);
} else {
widgetType.setTenantId(getCurrentUser().getTenantId());
widgetTypeDetails.setTenantId(getCurrentUser().getTenantId());
}
checkEntity(widgetType.getId(), widgetType, Resource.WIDGET_TYPE);
checkEntity(widgetTypeDetails.getId(), widgetTypeDetails, Resource.WIDGET_TYPE);
return checkNotNull(widgetTypeService.saveWidgetType(widgetType));
return checkNotNull(widgetTypeService.saveWidgetType(widgetTypeDetails));
} catch (Exception e) {
throw handleException(e);
}
@ -107,6 +109,44 @@ public class WidgetTypeController extends BaseController {
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypesDetails", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET)
@ResponseBody
public List<WidgetTypeDetails> getBundleWidgetTypesDetails(
@RequestParam boolean isSystem,
@RequestParam String bundleAlias) throws ThingsboardException {
try {
TenantId tenantId;
if (isSystem) {
tenantId = TenantId.SYS_TENANT_ID;
} else {
tenantId = getCurrentUser().getTenantId();
}
return checkNotNull(widgetTypeService.findWidgetTypesDetailsByTenantIdAndBundleAlias(tenantId, bundleAlias));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
@RequestMapping(value = "/widgetTypesInfos", params = {"isSystem", "bundleAlias"}, method = RequestMethod.GET)
@ResponseBody
public List<WidgetTypeInfo> getBundleWidgetTypesInfos(
@RequestParam boolean isSystem,
@RequestParam String bundleAlias) throws ThingsboardException {
try {
TenantId tenantId;
if (isSystem) {
tenantId = TenantId.SYS_TENANT_ID;
} else {
tenantId = getCurrentUser().getTenantId();
}
return checkNotNull(widgetTypeService.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId, bundleAlias));
} catch (Exception e) {
throw handleException(e);
}
}
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetType", params = {"isSystem", "bundleAlias", "alias"}, method = RequestMethod.GET)
@ResponseBody

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;

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

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplat
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
@ -168,9 +169,9 @@ public class InstallScripts {
widgetTypesArrayJson.forEach(
widgetTypeJson -> {
try {
WidgetType widgetType = objectMapper.treeToValue(widgetTypeJson, WidgetType.class);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetTypeService.saveWidgetType(widgetType);
WidgetTypeDetails widgetTypeDetails = objectMapper.treeToValue(widgetTypeJson, WidgetTypeDetails.class);
widgetTypeDetails.setBundleAlias(savedWidgetsBundle.getAlias());
widgetTypeService.saveWidgetType(widgetTypeDetails);
} catch (Exception e) {
log.error("Unable to load widget type from json: [{}]", path.toString());
throw new RuntimeException("Unable to load widget type from json", e);

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);
}

51
application/src/test/java/org/thingsboard/server/controller/BaseWidgetTypeControllerTest.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import java.util.ArrayList;
@ -76,11 +77,11 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
@Test
public void testSaveWidgetType() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
Assert.assertNotNull(savedWidgetType);
Assert.assertNotNull(savedWidgetType.getId());
@ -95,42 +96,42 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
doPost("/api/widgetType", savedWidgetType, WidgetType.class);
WidgetType foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetType.class);
WidgetTypeDetails foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetTypeDetails.class);
Assert.assertEquals(foundWidgetType.getName(), savedWidgetType.getName());
}
@Test
public void testUpdateWidgetTypeFromDifferentTenant() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
loginDifferentTenant();
doPost("/api/widgetType", savedWidgetType, WidgetType.class, status().isForbidden());
doPost("/api/widgetType", savedWidgetType, WidgetTypeDetails.class, status().isForbidden());
deleteDifferentTenant();
}
@Test
public void testFindWidgetTypeById() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
WidgetType foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetType.class);
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
WidgetTypeDetails foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetTypeDetails.class);
Assert.assertNotNull(foundWidgetType);
Assert.assertEquals(savedWidgetType, foundWidgetType);
}
@Test
public void testDeleteWidgetType() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
doDelete("/api/widgetType/"+savedWidgetType.getId().getId().toString())
.andExpect(status().isOk());
@ -141,7 +142,7 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
@Test
public void testSaveWidgetTypeWithEmptyName() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
doPost("/api/widgetType", widgetType)
@ -151,7 +152,7 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
@Test
public void testSaveWidgetTypeWithEmptyBundleAlias() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
doPost("/api/widgetType", widgetType)
@ -161,7 +162,7 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
@Test
public void testSaveWidgetTypeWithEmptyDescriptor() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{}", JsonNode.class));
@ -172,7 +173,7 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
@Test
public void testSaveWidgetTypeWithInvalidBundleAlias() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias("some_alias");
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
@ -183,11 +184,11 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
@Test
public void testUpdateWidgetTypeBundleAlias() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
savedWidgetType.setBundleAlias("some_alias");
doPost("/api/widgetType", savedWidgetType)
.andExpect(status().isBadRequest())
@ -197,11 +198,11 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
@Test
public void testUpdateWidgetTypeAlias() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
savedWidgetType.setAlias("some_alias");
doPost("/api/widgetType", savedWidgetType)
.andExpect(status().isBadRequest())
@ -213,15 +214,15 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
public void testGetBundleWidgetTypes() throws Exception {
List<WidgetType> widgetTypes = new ArrayList<>();
for (int i=0;i<89;i++) {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type " + i);
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
widgetTypes.add(doPost("/api/widgetType", widgetType, WidgetType.class));
widgetTypes.add(new WidgetType(doPost("/api/widgetType", widgetType, WidgetTypeDetails.class)));
}
List<WidgetType> loadedWidgetTypes = doGetTyped("/api/widgetTypes?isSystem={isSystem}&bundleAlias={bundleAlias}",
new TypeReference<List<WidgetType>>(){}, false, savedWidgetsBundle.getAlias());
new TypeReference<>(){}, false, savedWidgetsBundle.getAlias());
Collections.sort(widgetTypes, idComparator);
Collections.sort(loadedWidgetTypes, idComparator);
@ -231,15 +232,15 @@ public abstract class BaseWidgetTypeControllerTest extends AbstractControllerTes
@Test
public void testGetWidgetType() throws Exception {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
WidgetTypeDetails savedWidgetType = doPost("/api/widgetType", widgetType, WidgetTypeDetails.class);
WidgetType foundWidgetType = doGet("/api/widgetType?isSystem={isSystem}&bundleAlias={bundleAlias}&alias={alias}",
WidgetType.class, false, savedWidgetsBundle.getAlias(), savedWidgetType.getAlias());
Assert.assertNotNull(foundWidgetType);
Assert.assertEquals(savedWidgetType, foundWidgetType);
Assert.assertEquals(new WidgetType(savedWidgetType), foundWidgetType);
}
}

10
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java

@ -18,6 +18,8 @@ package org.thingsboard.server.dao.widget;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import java.util.List;
@ -25,12 +27,18 @@ public interface WidgetTypeService {
WidgetType findWidgetTypeById(TenantId tenantId, WidgetTypeId widgetTypeId);
WidgetType saveWidgetType(WidgetType widgetType);
WidgetTypeDetails findWidgetTypeDetailsById(TenantId tenantId, WidgetTypeId widgetTypeId);
WidgetTypeDetails saveWidgetType(WidgetTypeDetails widgetType);
void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId);
List<WidgetType> findWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);
List<WidgetTypeDetails> findWidgetTypesDetailsByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);
List<WidgetTypeInfo> findWidgetTypesInfosByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);
WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias);
void deleteWidgetTypesByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias);

49
common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java

@ -0,0 +1,49 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.widget;
import lombok.Data;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
@Data
public class BaseWidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
private static final long serialVersionUID = 8388684344603660756L;
private TenantId tenantId;
private String bundleAlias;
private String alias;
private String name;
public BaseWidgetType() {
super();
}
public BaseWidgetType(WidgetTypeId id) {
super(id);
}
public BaseWidgetType(BaseWidgetType widgetType) {
super(widgetType);
this.tenantId = widgetType.getTenantId();
this.bundleAlias = widgetType.getBundleAlias();
this.alias = widgetType.getAlias();
this.name = widgetType.getName();
}
}

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

@ -16,21 +16,12 @@
package org.thingsboard.server.common.data.widget;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.TenantId;
import lombok.Data;
import org.thingsboard.server.common.data.id.WidgetTypeId;
@EqualsAndHashCode(callSuper = true)
public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
@Data
public class WidgetType extends BaseWidgetType {
private static final long serialVersionUID = 8388684344603660756L;
private TenantId tenantId;
private String bundleAlias;
private String alias;
private String name;
private transient JsonNode descriptor;
public WidgetType() {
@ -41,65 +32,13 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
super(id);
}
public WidgetType(BaseWidgetType baseWidgetType) {
super(baseWidgetType);
}
public WidgetType(WidgetType widgetType) {
super(widgetType);
this.tenantId = widgetType.getTenantId();
this.bundleAlias = widgetType.getBundleAlias();
this.alias = widgetType.getAlias();
this.name = widgetType.getName();
this.descriptor = widgetType.getDescriptor();
}
public TenantId getTenantId() {
return tenantId;
}
public void setTenantId(TenantId tenantId) {
this.tenantId = tenantId;
}
public String getBundleAlias() {
return bundleAlias;
}
public void setBundleAlias(String bundleAlias) {
this.bundleAlias = bundleAlias;
}
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public JsonNode getDescriptor() {
return descriptor;
}
public void setDescriptor(JsonNode descriptor) {
this.descriptor = descriptor;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("WidgetType{");
sb.append("tenantId=").append(tenantId);
sb.append(", bundleAlias='").append(bundleAlias).append('\'');
sb.append(", alias='").append(alias).append('\'');
sb.append(", name='").append(name).append('\'');
sb.append(", descriptor=").append(descriptor);
sb.append('}');
return sb.toString();
}
}

45
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java

@ -0,0 +1,45 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.widget;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.id.WidgetTypeId;
@Data
public class WidgetTypeDetails extends WidgetType {
private String image;
private String description;
public WidgetTypeDetails() {
super();
}
public WidgetTypeDetails(WidgetTypeId id) {
super(id);
}
public WidgetTypeDetails(BaseWidgetType baseWidgetType) {
super(baseWidgetType);
}
public WidgetTypeDetails(WidgetTypeDetails widgetTypeDetails) {
super(widgetTypeDetails);
this.image = widgetTypeDetails.getImage();
this.description = widgetTypeDetails.getDescription();
}
}

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

@ -0,0 +1,46 @@
/**
* 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.
*/
package org.thingsboard.server.common.data.widget;
import lombok.Data;
import org.thingsboard.server.common.data.id.WidgetTypeId;
@Data
public class WidgetTypeInfo extends BaseWidgetType {
private String image;
private String description;
private String widgetType;
public WidgetTypeInfo() {
super();
}
public WidgetTypeInfo(WidgetTypeId id) {
super(id);
}
public WidgetTypeInfo(BaseWidgetType baseWidgetType) {
super(baseWidgetType);
}
public WidgetTypeInfo(WidgetTypeInfo widgetTypeInfo) {
super(widgetTypeInfo);
this.image = widgetTypeInfo.getImage();
this.description = widgetTypeInfo.getDescription();
this.widgetType = widgetTypeInfo.getWidgetType();
}
}

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";

86
dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractWidgetTypeEntity.java

@ -0,0 +1,86 @@
/**
* 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.
*/
package org.thingsboard.server.dao.model.sql;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
import java.util.UUID;
@Data
@EqualsAndHashCode(callSuper = true)
@MappedSuperclass
public abstract class AbstractWidgetTypeEntity<T extends BaseWidgetType> extends BaseSqlEntity<T> implements BaseEntity<T> {
@Column(name = ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY)
private UUID tenantId;
@Column(name = ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY)
private String bundleAlias;
@Column(name = ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY)
private String alias;
@Column(name = ModelConstants.WIDGET_TYPE_NAME_PROPERTY)
private String name;
public AbstractWidgetTypeEntity() {
super();
}
public AbstractWidgetTypeEntity(BaseWidgetType widgetType) {
if (widgetType.getId() != null) {
this.setUuid(widgetType.getId().getId());
}
this.setCreatedTime(widgetType.getCreatedTime());
if (widgetType.getTenantId() != null) {
this.tenantId = widgetType.getTenantId().getId();
}
this.bundleAlias = widgetType.getBundleAlias();
this.alias = widgetType.getAlias();
this.name = widgetType.getName();
}
public AbstractWidgetTypeEntity(AbstractWidgetTypeEntity widgetTypeEntity) {
this.setId(widgetTypeEntity.getId());
this.setCreatedTime(widgetTypeEntity.getCreatedTime());
this.tenantId = widgetTypeEntity.getTenantId();
this.bundleAlias = widgetTypeEntity.getBundleAlias();
this.alias = widgetTypeEntity.getAlias();
this.name = widgetTypeEntity.getName();
}
protected BaseWidgetType toBaseWidgetType() {
BaseWidgetType widgetType = new BaseWidgetType(new WidgetTypeId(getUuid()));
widgetType.setCreatedTime(createdTime);
if (tenantId != null) {
widgetType.setTenantId(new TenantId(tenantId));
}
widgetType.setBundleAlias(bundleAlias);
widgetType.setAlias(alias);
widgetType.setName(name);
return widgetType;
}
}

69
dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java

@ -0,0 +1,69 @@
/**
* 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.
*/
package org.thingsboard.server.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@TypeDef(name = "json", typeClass = JsonStringType.class)
@Table(name = ModelConstants.WIDGET_TYPE_COLUMN_FAMILY_NAME)
public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity<WidgetTypeDetails> {
@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;
public WidgetTypeDetailsEntity() {
super();
}
public WidgetTypeDetailsEntity(WidgetTypeDetails widgetTypeDetails) {
super(widgetTypeDetails);
this.image = widgetTypeDetails.getImage();
this.description = widgetTypeDetails.getDescription();
this.descriptor = widgetTypeDetails.getDescriptor();
}
@Override
public WidgetTypeDetails toData() {
BaseWidgetType baseWidgetType = super.toBaseWidgetType();
WidgetTypeDetails widgetTypeDetails = new WidgetTypeDetails(baseWidgetType);
widgetTypeDetails.setImage(image);
widgetTypeDetails.setDescription(description);
widgetTypeDetails.setDescriptor(descriptor);
return widgetTypeDetails;
}
}

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

@ -20,11 +20,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
@ -38,19 +35,7 @@ import java.util.UUID;
@Entity
@TypeDef(name = "json", typeClass = JsonStringType.class)
@Table(name = ModelConstants.WIDGET_TYPE_COLUMN_FAMILY_NAME)
public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implements BaseEntity<WidgetType> {
@Column(name = ModelConstants.WIDGET_TYPE_TENANT_ID_PROPERTY)
private UUID tenantId;
@Column(name = ModelConstants.WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY)
private String bundleAlias;
@Column(name = ModelConstants.WIDGET_TYPE_ALIAS_PROPERTY)
private String alias;
@Column(name = ModelConstants.WIDGET_TYPE_NAME_PROPERTY)
private String name;
public final class WidgetTypeEntity extends AbstractWidgetTypeEntity<WidgetType> {
@Type(type="json")
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY)
@ -60,30 +45,10 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement
super();
}
public WidgetTypeEntity(WidgetType widgetType) {
if (widgetType.getId() != null) {
this.setUuid(widgetType.getId().getId());
}
this.setCreatedTime(widgetType.getCreatedTime());
if (widgetType.getTenantId() != null) {
this.tenantId = widgetType.getTenantId().getId();
}
this.bundleAlias = widgetType.getBundleAlias();
this.alias = widgetType.getAlias();
this.name = widgetType.getName();
this.descriptor = widgetType.getDescriptor();
}
@Override
public WidgetType toData() {
WidgetType widgetType = new WidgetType(new WidgetTypeId(this.getUuid()));
widgetType.setCreatedTime(createdTime);
if (tenantId != null) {
widgetType.setTenantId(new TenantId(tenantId));
}
widgetType.setBundleAlias(bundleAlias);
widgetType.setAlias(alias);
widgetType.setName(name);
BaseWidgetType baseWidgetType = super.toBaseWidgetType();
WidgetType widgetType = new WidgetType(baseWidgetType);
widgetType.setDescriptor(descriptor);
return widgetType;
}

56
dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeInfoEntity.java

@ -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.
*/
package org.thingsboard.server.dao.model.sql;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
@Data
@EqualsAndHashCode(callSuper = true)
public final class WidgetTypeInfoEntity extends AbstractWidgetTypeEntity<WidgetTypeInfo> {
private String image;
private String description;
private String widgetType;
public WidgetTypeInfoEntity() {
super();
}
public WidgetTypeInfoEntity(WidgetTypeDetailsEntity widgetTypeDetailsEntity) {
super(widgetTypeDetailsEntity);
this.image = widgetTypeDetailsEntity.getImage();
this.description = widgetTypeDetailsEntity.getDescription();
if (widgetTypeDetailsEntity.getDescriptor() != null && widgetTypeDetailsEntity.getDescriptor().has("type")) {
this.widgetType = widgetTypeDetailsEntity.getDescriptor().get("type").asText();
} else {
this.widgetType = "";
}
}
@Override
public WidgetTypeInfo toData() {
BaseWidgetType baseWidgetType = super.toBaseWidgetType();
WidgetTypeInfo widgetTypeInfo = new WidgetTypeInfo(baseWidgetType);
widgetTypeInfo.setImage(image);
widgetTypeInfo.setDescription(description);
widgetTypeInfo.setWidgetType(widgetType);
return widgetTypeInfo;
}
}

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;
}
}

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

@ -18,8 +18,12 @@ package org.thingsboard.server.dao.sql.widget;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.WidgetTypeDetailsEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
@ -31,28 +35,43 @@ import java.util.UUID;
* Created by Valerii Sosliuk on 4/29/2017.
*/
@Component
public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeEntity, WidgetType> implements WidgetTypeDao {
public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, WidgetTypeDetails> implements WidgetTypeDao {
@Autowired
private WidgetTypeRepository widgetTypeRepository;
@Override
protected Class<WidgetTypeEntity> getEntityClass() {
return WidgetTypeEntity.class;
protected Class<WidgetTypeDetailsEntity> getEntityClass() {
return WidgetTypeDetailsEntity.class;
}
@Override
protected CrudRepository<WidgetTypeEntity, UUID> getCrudRepository() {
protected CrudRepository<WidgetTypeDetailsEntity, UUID> getCrudRepository() {
return widgetTypeRepository;
}
@Override
public WidgetType findWidgetTypeById(TenantId tenantId, UUID widgetTypeId) {
return DaoUtil.getData(widgetTypeRepository.findWidgetTypeById(widgetTypeId));
}
@Override
public List<WidgetType> findWidgetTypesByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesByTenantIdAndBundleAlias(tenantId, bundleAlias));
}
@Override
public List<WidgetTypeDetails> findWidgetTypesDetailsByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias) {
return DaoUtil.convertDataList(widgetTypeRepository.findByTenantIdAndBundleAlias(tenantId, bundleAlias));
}
@Override
public List<WidgetTypeInfo> findWidgetTypesInfosByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId, bundleAlias));
}
@Override
public WidgetType findByTenantIdBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias) {
return DaoUtil.getData(widgetTypeRepository.findByTenantIdAndBundleAliasAndAlias(tenantId, bundleAlias, alias));
return DaoUtil.getData(widgetTypeRepository.findWidgetTypeByTenantIdAndBundleAliasAndAlias(tenantId, bundleAlias, alias));
}
}

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

@ -15,18 +15,35 @@
*/
package org.thingsboard.server.dao.sql.widget;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sql.WidgetTypeDetailsEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity;
import java.util.List;
import java.util.UUID;
/**
* Created by Valerii Sosliuk on 4/29/2017.
*/
public interface WidgetTypeRepository extends CrudRepository<WidgetTypeEntity, UUID> {
public interface WidgetTypeRepository extends CrudRepository<WidgetTypeDetailsEntity, UUID> {
@Query("SELECT wt FROM WidgetTypeEntity wt WHERE wt.id = :widgetTypeId")
WidgetTypeEntity findWidgetTypeById(@Param("widgetTypeId") UUID widgetTypeId);
@Query("SELECT wt FROM WidgetTypeEntity wt WHERE wt.tenantId = :tenantId AND wt.bundleAlias = :bundleAlias")
List<WidgetTypeEntity> findWidgetTypesByTenantIdAndBundleAlias(@Param("tenantId") UUID tenantId,
@Param("bundleAlias") String bundleAlias);
@Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd " +
"WHERE wtd.tenantId = :tenantId AND wtd.bundleAlias = :bundleAlias")
List<WidgetTypeInfoEntity> findWidgetTypesInfosByTenantIdAndBundleAlias(@Param("tenantId") UUID tenantId,
@Param("bundleAlias") String bundleAlias);
List<WidgetTypeEntity> findByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias);
List<WidgetTypeDetailsEntity> findByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias);
WidgetTypeEntity findByTenantIdAndBundleAliasAndAlias(UUID tenantId, String bundleAlias, String alias);
@Query("SELECT wt FROM WidgetTypeEntity wt " +
"WHERE wt.tenantId = :tenantId AND wt.bundleAlias = :bundleAlias AND wt.alias = :alias")
WidgetTypeEntity findWidgetTypeByTenantIdAndBundleAliasAndAlias(@Param("tenantId") UUID tenantId,
@Param("bundleAlias") String bundleAlias,
@Param("alias") String alias);
}

35
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java

@ -17,6 +17,8 @@ package org.thingsboard.server.dao.widget;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.Dao;
import java.util.List;
@ -25,15 +27,24 @@ import java.util.UUID;
/**
* The Interface WidgetTypeDao.
*/
public interface WidgetTypeDao extends Dao<WidgetType> {
public interface WidgetTypeDao extends Dao<WidgetTypeDetails> {
/**
* Save or update widget type object
*
* @param widgetType the widget type object
* @param widgetTypeDetails the widget type details object
* @return saved widget type object
*/
WidgetType save(TenantId tenantId, WidgetType widgetType);
WidgetTypeDetails save(TenantId tenantId, WidgetTypeDetails widgetTypeDetails);
/**
* Find widget type by tenantId and widgetTypeId.
*
* @param tenantId the tenantId
* @param widgetTypeId the widget type id
* @return the widget type object
*/
WidgetType findWidgetTypeById(TenantId tenantId, UUID widgetTypeId);
/**
* Find widget types by tenantId and bundleAlias.
@ -44,6 +55,24 @@ public interface WidgetTypeDao extends Dao<WidgetType> {
*/
List<WidgetType> findWidgetTypesByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias);
/**
* Find widget types details by tenantId and bundleAlias.
*
* @param tenantId the tenantId
* @param bundleAlias the bundle alias
* @return the list of widget types details objects
*/
List<WidgetTypeDetails> findWidgetTypesDetailsByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias);
/**
* Find widget types infos by tenantId and bundleAlias.
*
* @param tenantId the tenantId
* @param bundleAlias the bundle alias
* @return the list of widget types infos objects
*/
List<WidgetTypeInfo> findWidgetTypesInfosByTenantIdAndBundleAlias(UUID tenantId, String bundleAlias);
/**
* Find widget type by tenantId, bundleAlias and alias.
*

75
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
@ -51,14 +53,21 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
public WidgetType findWidgetTypeById(TenantId tenantId, WidgetTypeId widgetTypeId) {
log.trace("Executing findWidgetTypeById [{}]", widgetTypeId);
Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
return widgetTypeDao.findWidgetTypeById(tenantId, widgetTypeId.getId());
}
@Override
public WidgetTypeDetails findWidgetTypeDetailsById(TenantId tenantId, WidgetTypeId widgetTypeId) {
log.trace("Executing findWidgetTypeDetailsById [{}]", widgetTypeId);
Validator.validateId(widgetTypeId, "Incorrect widgetTypeId " + widgetTypeId);
return widgetTypeDao.findById(tenantId, widgetTypeId.getId());
}
@Override
public WidgetType saveWidgetType(WidgetType widgetType) {
log.trace("Executing saveWidgetType [{}]", widgetType);
widgetTypeValidator.validate(widgetType, WidgetType::getTenantId);
return widgetTypeDao.save(widgetType.getTenantId(), widgetType);
public WidgetTypeDetails saveWidgetType(WidgetTypeDetails widgetTypeDetails) {
log.trace("Executing saveWidgetType [{}]", widgetTypeDetails);
widgetTypeValidator.validate(widgetTypeDetails, WidgetType::getTenantId);
return widgetTypeDao.save(widgetTypeDetails.getTenantId(), widgetTypeDetails);
}
@Override
@ -76,6 +85,22 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
return widgetTypeDao.findWidgetTypesByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias);
}
@Override
public List<WidgetTypeDetails> findWidgetTypesDetailsByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias) {
log.trace("Executing findWidgetTypesDetailsByTenantIdAndBundleAlias, tenantId [{}], bundleAlias [{}]", tenantId, bundleAlias);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validateString(bundleAlias, INCORRECT_BUNDLE_ALIAS + bundleAlias);
return widgetTypeDao.findWidgetTypesDetailsByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias);
}
@Override
public List<WidgetTypeInfo> findWidgetTypesInfosByTenantIdAndBundleAlias(TenantId tenantId, String bundleAlias) {
log.trace("Executing findWidgetTypesInfosByTenantIdAndBundleAlias, tenantId [{}], bundleAlias [{}]", tenantId, bundleAlias);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validateString(bundleAlias, INCORRECT_BUNDLE_ALIAS + bundleAlias);
return widgetTypeDao.findWidgetTypesInfosByTenantIdAndBundleAlias(tenantId.getId(), bundleAlias);
}
@Override
public WidgetType findWidgetTypeByTenantIdBundleAliasAndAlias(TenantId tenantId, String bundleAlias, String alias) {
log.trace("Executing findWidgetTypeByTenantIdBundleAliasAndAlias, tenantId [{}], bundleAlias [{}], alias [{}]", tenantId, bundleAlias, alias);
@ -96,24 +121,24 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
}
}
private DataValidator<WidgetType> widgetTypeValidator =
new DataValidator<WidgetType>() {
private DataValidator<WidgetTypeDetails> widgetTypeValidator =
new DataValidator<>() {
@Override
protected void validateDataImpl(TenantId tenantId, WidgetType widgetType) {
if (StringUtils.isEmpty(widgetType.getName())) {
protected void validateDataImpl(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) {
if (StringUtils.isEmpty(widgetTypeDetails.getName())) {
throw new DataValidationException("Widgets type name should be specified!");
}
if (StringUtils.isEmpty(widgetType.getBundleAlias())) {
if (StringUtils.isEmpty(widgetTypeDetails.getBundleAlias())) {
throw new DataValidationException("Widgets type bundle alias should be specified!");
}
if (widgetType.getDescriptor() == null || widgetType.getDescriptor().size() == 0) {
if (widgetTypeDetails.getDescriptor() == null || widgetTypeDetails.getDescriptor().size() == 0) {
throw new DataValidationException("Widgets type descriptor can't be empty!");
}
if (widgetType.getTenantId() == null) {
widgetType.setTenantId(new TenantId(ModelConstants.NULL_UUID));
if (widgetTypeDetails.getTenantId() == null) {
widgetTypeDetails.setTenantId(new TenantId(ModelConstants.NULL_UUID));
}
if (!widgetType.getTenantId().getId().equals(ModelConstants.NULL_UUID)) {
Tenant tenant = tenantDao.findById(tenantId, widgetType.getTenantId().getId());
if (!widgetTypeDetails.getTenantId().getId().equals(ModelConstants.NULL_UUID)) {
Tenant tenant = tenantDao.findById(tenantId, widgetTypeDetails.getTenantId().getId());
if (tenant == null) {
throw new DataValidationException("Widget type is referencing to non-existent tenant!");
}
@ -121,37 +146,37 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
}
@Override
protected void validateCreate(TenantId tenantId, WidgetType widgetType) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(widgetType.getTenantId().getId(), widgetType.getBundleAlias());
protected void validateCreate(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(widgetTypeDetails.getTenantId().getId(), widgetTypeDetails.getBundleAlias());
if (widgetsBundle == null) {
throw new DataValidationException("Widget type is referencing to non-existent widgets bundle!");
}
String alias = widgetType.getAlias();
String alias = widgetTypeDetails.getAlias();
if (alias == null || alias.trim().isEmpty()) {
alias = widgetType.getName().toLowerCase().replaceAll("\\W+", "_");
alias = widgetTypeDetails.getName().toLowerCase().replaceAll("\\W+", "_");
}
String originalAlias = alias;
int c = 1;
WidgetType withSameAlias;
do {
withSameAlias = widgetTypeDao.findByTenantIdBundleAliasAndAlias(widgetType.getTenantId().getId(), widgetType.getBundleAlias(), alias);
withSameAlias = widgetTypeDao.findByTenantIdBundleAliasAndAlias(widgetTypeDetails.getTenantId().getId(), widgetTypeDetails.getBundleAlias(), alias);
if (withSameAlias != null) {
alias = originalAlias + (++c);
}
} while (withSameAlias != null);
widgetType.setAlias(alias);
widgetTypeDetails.setAlias(alias);
}
@Override
protected void validateUpdate(TenantId tenantId, WidgetType widgetType) {
WidgetType storedWidgetType = widgetTypeDao.findById(tenantId, widgetType.getId().getId());
if (!storedWidgetType.getTenantId().getId().equals(widgetType.getTenantId().getId())) {
protected void validateUpdate(TenantId tenantId, WidgetTypeDetails widgetTypeDetails) {
WidgetType storedWidgetType = widgetTypeDao.findById(tenantId, widgetTypeDetails.getId().getId());
if (!storedWidgetType.getTenantId().getId().equals(widgetTypeDetails.getTenantId().getId())) {
throw new DataValidationException("Can't move existing widget type to different tenant!");
}
if (!storedWidgetType.getBundleAlias().equals(widgetType.getBundleAlias())) {
if (!storedWidgetType.getBundleAlias().equals(widgetTypeDetails.getBundleAlias())) {
throw new DataValidationException("Update of widget type bundle alias is prohibited!");
}
if (!storedWidgetType.getAlias().equals(widgetType.getAlias())) {
if (!storedWidgetType.getAlias().equals(widgetTypeDetails.getAlias())) {
throw new DataValidationException("Update of widget type alias is prohibited!");
}
}

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 (

43
dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java

@ -25,6 +25,7 @@ import org.junit.Test;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
@ -62,12 +63,12 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
Assert.assertNotNull(savedWidgetType);
Assert.assertNotNull(savedWidgetType.getId());
@ -94,7 +95,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
widgetsBundle.setTitle("Widgets bundle");
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
@ -107,7 +108,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
@Test(expected = DataValidationException.class)
public void testSaveWidgetTypeWithEmptyBundleAlias() throws IOException {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
@ -121,7 +122,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
widgetsBundle.setTitle("Widgets bundle");
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setName("Widget Type");
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
@ -140,7 +141,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
widgetsBundle.setTitle("Widgets bundle");
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(new TenantId(Uuids.timeBased()));
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
@ -154,7 +155,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
@Test(expected = DataValidationException.class)
public void testSaveWidgetTypeWithInvalidBundleAlias() throws IOException {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias("some_alias");
widgetType.setName("Widget Type");
@ -169,12 +170,12 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
widgetsBundle.setTitle("Widgets bundle");
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
savedWidgetType.setTenantId(new TenantId(ModelConstants.NULL_UUID));
try {
widgetTypeService.saveWidgetType(savedWidgetType);
@ -190,12 +191,12 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
widgetsBundle.setTitle("Widgets bundle");
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
savedWidgetType.setBundleAlias("some_alias");
try {
widgetTypeService.saveWidgetType(savedWidgetType);
@ -211,12 +212,12 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
widgetsBundle.setTitle("Widgets bundle");
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
savedWidgetType.setAlias("some_alias");
try {
widgetTypeService.saveWidgetType(savedWidgetType);
@ -232,13 +233,13 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
widgetsBundle.setTitle("Widgets bundle");
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
WidgetType foundWidgetType = widgetTypeService.findWidgetTypeById(tenantId, savedWidgetType.getId());
WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
WidgetTypeDetails foundWidgetType = widgetTypeService.findWidgetTypeDetailsById(tenantId, savedWidgetType.getId());
Assert.assertNotNull(foundWidgetType);
Assert.assertEquals(savedWidgetType, foundWidgetType);
@ -252,12 +253,12 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
widgetsBundle.setTitle("Widgets bundle");
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
WidgetType savedWidgetType = widgetTypeService.saveWidgetType(widgetType);
WidgetType savedWidgetType = new WidgetType(widgetTypeService.saveWidgetType(widgetType));
WidgetType foundWidgetType = widgetTypeService.findWidgetTypeByTenantIdBundleAliasAndAlias(tenantId, savedWidgetsBundle.getAlias(), savedWidgetType.getAlias());
Assert.assertNotNull(foundWidgetType);
Assert.assertEquals(savedWidgetType, foundWidgetType);
@ -272,7 +273,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
widgetsBundle.setTitle("Widgets bundle");
WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle);
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type");
@ -296,12 +297,12 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest {
List<WidgetType> widgetTypes = new ArrayList<>();
for (int i=0;i<121;i++) {
WidgetType widgetType = new WidgetType();
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
widgetType.setName("Widget Type " + i);
widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
widgetTypes.add(widgetTypeService.saveWidgetType(widgetType));
widgetTypes.add(new WidgetType(widgetTypeService.saveWidgetType(widgetType)));
}
List<WidgetType> loadedWidgetTypes = widgetTypeService.findWidgetTypesByTenantIdAndBundleAlias(tenantId, savedWidgetsBundle.getAlias());

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

@ -21,11 +21,23 @@ import { HttpClient } from '@angular/common/http';
import { PageLink } from '@shared/models/page/page-link';
import { PageData } from '@shared/models/page/page-data';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { Widget, WidgetType, widgetType, widgetTypesData } from '@shared/models/widget.models';
import {
Widget,
WidgetType,
widgetType,
WidgetTypeDetails,
WidgetTypeInfo,
widgetTypesData
} from '@shared/models/widget.models';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { ResourcesService } from '../services/resources.service';
import { toWidgetInfo, toWidgetType, WidgetInfo } from '@app/modules/home/models/widget-component.models';
import {
toWidgetInfo,
toWidgetType,
toWidgetTypeDetails,
WidgetInfo
} from '@app/modules/home/models/widget-component.models';
import { filter, map, mergeMap, tap } from 'rxjs/operators';
import { WidgetTypeId } from '@shared/models/id/widget-type-id';
import { NULL_UUID } from '@shared/models/id/has-uuid';
@ -117,6 +129,18 @@ export class WidgetService {
defaultHttpOptionsFromConfig(config));
}
public getBundleWidgetTypesDetails(bundleAlias: string, isSystem: boolean,
config?: RequestConfig): Observable<Array<WidgetTypeDetails>> {
return this.http.get<Array<WidgetTypeDetails>>(`/api/widgetTypesDetails?isSystem=${isSystem}&bundleAlias=${bundleAlias}`,
defaultHttpOptionsFromConfig(config));
}
public getBundleWidgetTypeInfos(bundleAlias: string, isSystem: boolean,
config?: RequestConfig): Observable<Array<WidgetTypeInfo>> {
return this.http.get<Array<WidgetTypeInfo>>(`/api/widgetTypesInfos?isSystem=${isSystem}&bundleAlias=${bundleAlias}`,
defaultHttpOptionsFromConfig(config));
}
public loadBundleLibraryWidgets(bundleAlias: string, isSystem: boolean,
config?: RequestConfig): Observable<Array<Widget>> {
return this.getBundleWidgetTypes(bundleAlias, isSystem, config).pipe(
@ -176,21 +200,22 @@ export class WidgetService {
defaultHttpOptionsFromConfig(config));
}
public saveWidgetType(widgetInfo: WidgetInfo,
id: WidgetTypeId,
bundleAlias: string,
config?: RequestConfig): Observable<WidgetType> {
const widgetTypeInstance = toWidgetType(widgetInfo, id, undefined, bundleAlias);
return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance,
public saveWidgetTypeDetails(widgetInfo: WidgetInfo,
id: WidgetTypeId,
bundleAlias: string,
createdTime: number,
config?: RequestConfig): Observable<WidgetTypeDetails> {
const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, bundleAlias, createdTime);
return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails,
defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => {
this.widgetTypeUpdatedSubject.next(savedWidgetType);
}));
}
public saveImportedWidgetType(widgetTypeInstance: WidgetType,
config?: RequestConfig): Observable<WidgetType> {
return this.http.post<WidgetType>('/api/widgetType', widgetTypeInstance,
public saveImportedWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails,
config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails,
defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => {
this.widgetTypeUpdatedSubject.next(savedWidgetType);
@ -212,8 +237,8 @@ export class WidgetService {
}
public getWidgetTypeById(widgetTypeId: string,
config?: RequestConfig): Observable<WidgetType> {
return this.http.get<WidgetType>(`/api/widgetType/${widgetTypeId}`,
config?: RequestConfig): Observable<WidgetTypeDetails> {
return this.http.get<WidgetTypeDetails>(`/api/widgetType/${widgetTypeId}`,
defaultHttpOptionsFromConfig(config));
}

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,

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%;
}
}
}
}

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

@ -14,120 +14,164 @@
/// 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.getBundleWidgetTypeInfos(bundleAlias, isSystem).pipe(
map(widgets => widgets.sort((a, b) => b.createdTime - a.createdTime)),
map(widgets => widgets.map((widgetTypeInfo) => {
this.widgetsType.add(widgetTypeInfo.widgetType);
const widget: WidgetInfo = {
isSystemType: isSystem,
bundleAlias,
typeAlias: widgetTypeInfo.alias,
type: widgetTypeInfo.widgetType,
title: widgetTypeInfo.name,
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,

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

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Widget, WidgetType } from '@app/shared/models/widget.models';
import { Widget, WidgetType, WidgetTypeDetails } from '@app/shared/models/widget.models';
import { DashboardLayoutId } from '@shared/models/dashboard.models';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
@ -25,7 +25,7 @@ export interface ImportWidgetResult {
export interface WidgetsBundleItem {
widgetsBundle: WidgetsBundle;
widgetTypes: WidgetType[];
widgetTypes: WidgetTypeDetails[];
}
export interface CsvToJsonConfig {

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

@ -38,7 +38,7 @@ import { forkJoin, Observable, of } from 'rxjs';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { EntityService } from '@core/http/entity.service';
import { Widget, WidgetSize, WidgetType } from '@shared/models/widget.models';
import { Widget, WidgetSize, WidgetType, WidgetTypeDetails } from '@shared/models/widget.models';
import {
EntityAliasesDialogComponent,
EntityAliasesDialogData
@ -234,13 +234,13 @@ export class ImportExportService {
public exportWidgetType(widgetTypeId: string) {
this.widgetService.getWidgetTypeById(widgetTypeId).subscribe(
(widgetType) => {
if (isDefined(widgetType.bundleAlias)) {
delete widgetType.bundleAlias;
(widgetTypeDetails) => {
if (isDefined(widgetTypeDetails.bundleAlias)) {
delete widgetTypeDetails.bundleAlias;
}
let name = widgetType.name;
let name = widgetTypeDetails.name;
name = name.toLowerCase().replace(/\W/g, '_');
this.exportToPc(this.prepareExport(widgetType), name);
this.exportToPc(this.prepareExport(widgetTypeDetails), name);
},
(e) => {
this.handleExportError(e, 'widget-type.export-failed-error');
@ -250,15 +250,15 @@ export class ImportExportService {
public importWidgetType(bundleAlias: string): Observable<WidgetType> {
return this.openImportDialog('widget-type.import', 'widget-type.widget-type-file').pipe(
mergeMap((widgetType: WidgetType) => {
if (!this.validateImportedWidgetType(widgetType)) {
mergeMap((widgetTypeDetails: WidgetTypeDetails) => {
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) {
this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant('widget-type.invalid-widget-type-file-error'),
type: 'error'}));
throw new Error('Invalid widget type file');
} else {
widgetType.bundleAlias = bundleAlias;
return this.widgetService.saveImportedWidgetType(widgetType);
widgetTypeDetails.bundleAlias = bundleAlias;
return this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails);
}
}),
catchError((err) => {
@ -272,17 +272,18 @@ export class ImportExportService {
(widgetsBundle) => {
const bundleAlias = widgetsBundle.alias;
const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe(
(widgetTypes) => {
this.widgetService.getBundleWidgetTypesDetails(bundleAlias, isSystem).subscribe(
(widgetTypesDetails) => {
widgetTypesDetails = widgetTypesDetails.sort((a, b) => a.createdTime - b.createdTime);
const widgetsBundleItem: WidgetsBundleItem = {
widgetsBundle: this.prepareExport(widgetsBundle),
widgetTypes: []
};
for (const widgetType of widgetTypes) {
if (isDefined(widgetType.bundleAlias)) {
delete widgetType.bundleAlias;
for (const widgetTypeDetails of widgetTypesDetails) {
if (isDefined(widgetTypeDetails.bundleAlias)) {
delete widgetTypeDetails.bundleAlias;
}
widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetType));
widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails));
}
let name = widgetsBundle.title;
name = name.toLowerCase().replace(/\W/g, '_');
@ -312,12 +313,12 @@ export class ImportExportService {
return this.widgetService.saveWidgetsBundle(widgetsBundle).pipe(
mergeMap((savedWidgetsBundle) => {
const bundleAlias = savedWidgetsBundle.alias;
const widgetTypes = widgetsBundleItem.widgetTypes;
if (widgetTypes.length) {
const widgetTypesDetails = widgetsBundleItem.widgetTypes;
if (widgetTypesDetails.length) {
const saveWidgetTypesObservables: Array<Observable<WidgetType>> = [];
for (const widgetType of widgetTypes) {
widgetType.bundleAlias = bundleAlias;
saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetType(widgetType));
for (const widgetTypeDetails of widgetTypesDetails) {
widgetTypeDetails.bundleAlias = bundleAlias;
saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetTypeDetails(widgetTypeDetails));
}
return forkJoin(saveWidgetTypesObservables).pipe(
map(() => savedWidgetsBundle)
@ -507,9 +508,9 @@ export class ImportExportService {
return true;
}
private validateImportedWidgetType(widgetType: WidgetType): boolean {
if (isUndefined(widgetType.name)
|| isUndefined(widgetType.descriptor)) {
private validateImportedWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails): boolean {
if (isUndefined(widgetTypeDetails.name)
|| isUndefined(widgetTypeDetails.descriptor)) {
return false;
}
return true;
@ -526,9 +527,9 @@ export class ImportExportService {
if (isUndefined(widgetsBundle.title)) {
return false;
}
const widgetTypes = widgetsBundleItem.widgetTypes;
for (const widgetType of widgetTypes) {
if (!this.validateImportedWidgetType(widgetType)) {
const widgetTypesDetails = widgetsBundleItem.widgetTypes;
for (const widgetTypeDetails of widgetTypesDetails) {
if (!this.validateImportedWidgetTypeDetails(widgetTypeDetails)) {
return false;
}
}

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

@ -105,7 +105,7 @@ export class WidgetComponentService {
settingsSchema: this.utils.editWidgetInfo.settingsSchema,
dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
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();

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

@ -27,7 +27,7 @@ import {
WidgetControllerDescriptor,
WidgetType,
widgetType,
WidgetTypeDescriptor,
WidgetTypeDescriptor, WidgetTypeDetails,
WidgetTypeParameters
} from '@shared/models/widget.models';
import { Timewindow, WidgetTimewindow } from '@shared/models/time/time.models';
@ -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' +
@ -421,6 +427,13 @@ export interface WidgetTypeInstance {
onDestroy?: () => void;
}
export function detailsToWidgetInfo(widgetTypeDetailsEntity: WidgetTypeDetails): WidgetInfo {
const widgetInfo = toWidgetInfo(widgetTypeDetailsEntity);
widgetInfo.image = widgetTypeDetailsEntity.image;
widgetInfo.description = widgetTypeDetailsEntity.description;
return widgetInfo;
}
export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
return {
widgetName: widgetTypeEntity.name,
@ -438,7 +451,18 @@ export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
};
}
export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, bundleAlias: string): WidgetType {
export function toWidgetTypeDetails(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
bundleAlias: string, createdTime: number): WidgetTypeDetails {
const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, bundleAlias, createdTime);
const widgetTypeDetails: WidgetTypeDetails = {...widgetTypeEntity,
description: widgetInfo.description,
image: widgetInfo.image
};
return widgetTypeDetails;
}
export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
bundleAlias: string, createdTime: number): WidgetType {
const descriptor: WidgetTypeDescriptor = {
type: widgetInfo.type,
sizeX: widgetInfo.sizeX,
@ -454,6 +478,7 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
return {
id,
tenantId,
createdTime,
bundleAlias,
alias: widgetInfo.alias,
name: widgetInfo.widgetName,

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>

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

@ -20,8 +20,15 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { WidgetService } from '@core/http/widget.service';
import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { Widget, WidgetConfig, WidgetType, widgetType, widgetTypesData } from '@shared/models/widget.models';
import { detailsToWidgetInfo, toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import {
Widget,
WidgetConfig,
WidgetType,
widgetType,
WidgetTypeDetails,
widgetTypesData
} from '@shared/models/widget.models';
import { ActivatedRoute, Router } from '@angular/router';
import { deepClone } from '@core/utils';
import { HasDirtyFlag } from '@core/guards/confirm-on-exit.guard';
@ -108,7 +115,7 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
isReadOnly: boolean;
widgetsBundle: WidgetsBundle;
widgetType: WidgetType;
widgetTypeDetails: WidgetTypeDetails;
widget: WidgetInfo;
origWidget: WidgetInfo;
@ -175,14 +182,14 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
} else {
this.isReadOnly = this.authUser.authority !== Authority.SYS_ADMIN;
}
this.widgetType = data.widgetEditorData.widgetType;
this.widgetTypeDetails = data.widgetEditorData.widgetTypeDetails;
this.widget = data.widgetEditorData.widget;
if (this.widgetType) {
if (this.widgetTypeDetails) {
const config = JSON.parse(this.widget.defaultConfig);
this.widget.defaultConfig = JSON.stringify(config);
}
this.origWidget = deepClone(this.widget);
if (!this.widgetType) {
if (!this.widgetTypeDetails) {
this.isDirty = true;
}
}
@ -515,10 +522,11 @@ 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(
(widgetTypeInstance) => {
this.setWidgetType(widgetTypeInstance);
const id = (this.widgetTypeDetails && this.widgetTypeDetails.id) ? this.widgetTypeDetails.id : undefined;
const createdTime = (this.widgetTypeDetails && this.widgetTypeDetails.createdTime) ? this.widgetTypeDetails.createdTime : undefined;
this.widgetService.saveWidgetTypeDetails(this.widget, id, this.widgetsBundle.alias, createdTime).subscribe(
(widgetTypeDetails) => {
this.setWidgetTypeDetails(widgetTypeDetails);
this.saveWidgetPending = false;
this.store.dispatch(new ActionNotificationShow(
{message: this.translate.instant('widget.widget-saved'), type: 'success', duration: 500}));
@ -543,9 +551,9 @@ 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(
(widgetTypeInstance) => {
this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeInstance.id.id}`);
this.widgetService.saveWidgetTypeDetails(this.widget, undefined, saveWidgetAsData.bundleAlias, undefined).subscribe(
(widgetTypeDetails) => {
this.router.navigateByUrl(`/widgets-bundles/${saveWidgetAsData.bundleId}/widgetTypes/${widgetTypeDetails.id.id}`);
}
);
}
@ -554,9 +562,9 @@ export class WidgetEditorComponent extends PageComponent implements OnInit, OnDe
);
}
private setWidgetType(widgetTypeInstance: WidgetType) {
this.widgetType = widgetTypeInstance;
this.widget = toWidgetInfo(this.widgetType);
private setWidgetTypeDetails(widgetTypeDetails: WidgetTypeDetails) {
this.widgetTypeDetails = widgetTypeDetails;
this.widget = detailsToWidgetInfo(this.widgetTypeDetails);
const config = JSON.parse(this.widget.defaultConfig);
this.widget.defaultConfig = JSON.stringify(config);
this.origWidget = deepClone(this.widget);

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

@ -27,14 +27,14 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { WidgetService } from '@core/http/widget.service';
import { WidgetEditorComponent } from '@home/pages/widget/widget-editor.component';
import { map } from 'rxjs/operators';
import { toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { widgetType, WidgetType } from '@app/shared/models/widget.models';
import { detailsToWidgetInfo, toWidgetInfo, WidgetInfo } from '@home/models/widget-component.models';
import { widgetType, WidgetType, WidgetTypeDetails } from '@app/shared/models/widget.models';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { WidgetsData } from '@home/models/dashboard-component.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
export interface WidgetEditorData {
widgetType: WidgetType;
widgetTypeDetails: WidgetTypeDetails;
widget: WidgetInfo;
}
@ -83,8 +83,8 @@ export class WidgetEditorDataResolver implements Resolve<WidgetEditorData> {
return this.widgetsService.getWidgetTypeById(widgetTypeId).pipe(
map((result) => {
return {
widgetType: result,
widget: toWidgetInfo(result)
widgetTypeDetails: result,
widget: detailsToWidgetInfo(result)
};
})
);
@ -106,7 +106,7 @@ export class WidgetEditorAddDataResolver implements Resolve<WidgetEditorData> {
map((widget) => {
widget.widgetName = null;
return {
widgetType: null,
widgetTypeDetails: null,
widget
};
})

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;
}
}
}

39
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: {
@ -165,14 +165,28 @@ export interface WidgetControllerDescriptor {
actionSources?: {[actionSourceId: string]: WidgetActionSource};
}
export interface WidgetType extends BaseData<WidgetTypeId> {
export interface BaseWidgetType extends BaseData<WidgetTypeId> {
tenantId: TenantId;
bundleAlias: string;
alias: string;
name: string;
}
export interface WidgetType extends BaseWidgetType {
descriptor: WidgetTypeDescriptor;
}
export interface WidgetTypeInfo extends BaseWidgetType {
image: string;
description: string;
widgetType: widgetType;
}
export interface WidgetTypeDetails extends WidgetType {
image: string;
description: string;
}
export enum LegendDirection {
column = 'column',
row = 'row'
@ -399,14 +413,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 +422,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 +448,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