Browse Source

Merge remote-tracking branch 'upstream/develop/3.5.2' into feature/change-user-profile

pull/8911/head
Vladyslav_Prykhodko 3 years ago
parent
commit
2d679c1416
  1. 42
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 47
      ui-ngx/src/app/app.component.ts
  3. 2
      ui-ngx/src/app/core/services/menu.models.ts
  4. 52
      ui-ngx/src/app/core/services/menu.service.ts
  5. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  6. 63
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  7. 29
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.scss
  8. 7
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  9. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  10. 3
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  11. 1
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  12. 3
      ui-ngx/src/app/modules/home/components/router-tabs.component.html
  13. 2
      ui-ngx/src/app/modules/home/components/router-tabs.component.scss
  14. 2
      ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html
  15. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  16. 28
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts
  17. 104
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  18. 276
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts
  19. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html
  20. 2
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html
  21. 365
      ui-ngx/src/app/modules/home/components/widget/config/widget-settings.models.ts
  22. 70
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html
  23. 70
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss
  24. 144
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts
  25. 127
      ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts
  26. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.html
  27. 2
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html
  28. 10
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html
  29. 6
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html
  30. 6
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html
  31. 1
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss
  32. 19
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html
  33. 7
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html
  34. 8
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
  35. 2
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html
  36. 3
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html
  37. 2
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss
  38. 6
      ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html
  39. 105
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html
  40. 85
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss
  41. 133
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts
  42. 30
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html
  43. 124
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts
  44. 22
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html
  45. 82
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts
  46. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html
  47. 148
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts
  48. 47
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.html
  49. 67
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.scss
  50. 70
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts
  51. 91
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
  52. 51
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss
  53. 133
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts
  54. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.html
  55. 102
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts
  56. 43
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html
  57. 116
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.scss
  58. 190
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts
  59. 37
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  60. 6
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  61. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  62. 14
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  63. 2
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  64. 7
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  65. 2
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
  66. 9
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss
  67. 6
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts
  68. 3
      ui-ngx/src/app/modules/home/menu/menu-link.component.html
  69. 3
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.html
  70. 3
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  71. 1
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  72. 1
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  73. 3
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  74. 3
      ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html
  75. 2
      ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss
  76. 8
      ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html
  77. 2
      ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss
  78. 6
      ui-ngx/src/app/shared/components/breadcrumb.component.html
  79. 2
      ui-ngx/src/app/shared/components/breadcrumb.component.ts
  80. 1
      ui-ngx/src/app/shared/components/breadcrumb.ts
  81. 4
      ui-ngx/src/app/shared/components/color-input.component.html
  82. 6
      ui-ngx/src/app/shared/components/color-input.component.scss
  83. 5
      ui-ngx/src/app/shared/components/help-markdown.component.ts
  84. 281
      ui-ngx/src/app/shared/components/icon.component.ts
  85. 8
      ui-ngx/src/app/shared/components/material-icon-select.component.html
  86. 18
      ui-ngx/src/app/shared/components/material-icon-select.component.scss
  87. 4
      ui-ngx/src/app/shared/components/material-icons.component.html
  88. 8
      ui-ngx/src/app/shared/components/notification/notification.component.html
  89. 1
      ui-ngx/src/app/shared/components/public-api.ts
  90. 2
      ui-ngx/src/app/shared/components/unit-input.component.html
  91. 63
      ui-ngx/src/app/shared/models/icon.models.ts
  92. 4
      ui-ngx/src/app/shared/models/widget.models.ts
  93. 8
      ui-ngx/src/app/shared/pipe/date-ago.pipe.ts
  94. 7
      ui-ngx/src/app/shared/shared.module.ts
  95. 88
      ui-ngx/src/assets/help/en_US/date/date-format.md
  96. 40
      ui-ngx/src/assets/help/en_US/widget/lib/card/value_color_fn.md
  97. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md
  98. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md
  99. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md
  100. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_color_fn.md

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

@ -225,6 +225,48 @@
"settingsDirective": "tb-dashboard-state-widget-settings",
"defaultConfig": "{\"datasources\":[{\"type\":\"static\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"syncParentStateParams\":true,\"defaultAutofillLayout\":true,\"defaultMargin\":0,\"defaultBackgroundColor\":\"#fff\"},\"title\":\"Dashboard state widget\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"showLegend\":false}"
}
},
{
"alias": "value_card",
"name": "Value card",
"image": null,
"description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.",
"descriptor": {
"type": "latest",
"sizeX": 2.5,
"sizeY": 2.5,
"resources": [],
"templateHtml": "<tb-value-card-widget \n [ctx]=\"ctx\">\n</tb-value-card-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '250px',\n previewHeight: '250px'\n };\n};\n\nself.onDestroy = function() {\n};\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-value-card-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}"
}
},
{
"alias": "horizontal_value_card",
"name": "Horizontal value card",
"image": null,
"description": "Designed to display single value of the selected attribute or timeseries data. Widget styles are customizable.",
"descriptor": {
"type": "latest",
"sizeX": 5,
"sizeY": 1.5,
"resources": [],
"templateHtml": "<tb-value-card-widget \n [ctx]=\"ctx\">\n</tb-value-card-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueCardWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n horizontal: true,\n previewWidth: '420px',\n previewHeight: '130px'\n };\n};\n\nself.onDestroy = function() {\n};\n",
"settingsSchema": "",
"dataKeySettingsSchema": "",
"settingsDirective": "",
"hasBasicMode": true,
"basicModeDirective": "tb-value-card-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Horizontal value card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\"}"
}
}
]
}

47
ui-ngx/src/app/app.component.ts

@ -30,6 +30,7 @@ import { combineLatest } from 'rxjs';
import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors';
import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators';
import { AuthService } from '@core/auth/auth.service';
import { svgIcons } from '@shared/models/icon.models';
@Component({
selector: 'tb-root',
@ -55,44 +56,14 @@ export class AppComponent implements OnInit {
}
});
this.matIconRegistry.addSvgIconLiteral(
'google-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'github-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 32.7 32.7"><path d="M16.3 0C7.3 0 0 7.3 0 16.3c0 7.2 4.7 13.3 11.1 15.5.8.1 1.1-.4 1.1-.8v-2.8c-4.5 1-5.5-2.2-5.5-2.2-.7-1.9-1.8-2.4-1.8-2.4-1.5-1 .1-1 .1-1 1.6.1 2.5 1.7 2.5 1.7 1.5 2.5 3.8 1.8 4.7 1.4.1-1.1.6-1.8 1-2.2-3.6-.4-7.4-1.8-7.4-8.1 0-1.8.6-3.2 1.7-4.4-.2-.4-.7-2.1.2-4.3 0 0 1.4-.4 4.5 1.7 1.3-.4 2.7-.5 4.1-.5s2.8.2 4.1.5c3.1-2.1 4.5-1.7 4.5-1.7.9 2.2.3 3.9.2 4.3 1 1.1 1.7 2.6 1.7 4.4 0 6.3-3.8 7.6-7.4 8 .6.5 1.1 1.5 1.1 3v4.5c0 .4.3.9 1.1.8 6.5-2.2 11.1-8.3 11.1-15.5C32.6 7.3 25.3 0 16.3 0z" fill="#211c19"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'facebook-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 263 263"><path d="M263 131.5C263 58.9 204.1 0 131.5 0S0 58.9 0 131.5c0 65.6 48.1 120 110.9 129.9v-91.9H77.5v-38h33.4v-29c0-33 19.6-51.2 49.7-51.2 14.4 0 29.4 2.6 29.4 2.6v32.4h-16.5c-16.3 0-21.4 10.1-21.4 20.5v24.7h36.4l-5.8 38h-30.6v91.9c62.8-9.9 110.9-64.3 110.9-129.9z" fill="#1877f2"/><path d="M182.7 169.5l5.8-38H152v-24.7c0-10.4 5.1-20.5 21.4-20.5H190V53.9s-15-2.6-29.4-2.6c-30 0-49.7 18.2-49.7 51.2v29H77.5v38h33.4v91.9c6.7 1.1 13.6 1.6 20.5 1.6s13.9-.5 20.5-1.6v-91.9h30.8z" fill="#fff"/></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'apple-logo',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg viewBox="0 0 256 315"><path d="M213.803394,167.030943 C214.2452,214.609646 255.542482,230.442639 256,230.644727 C255.650812,231.761357 249.401383,253.208293 234.24263,275.361446 C221.138555,294.513969 207.538253,313.596333 186.113759,313.991545 C165.062051,314.379442 158.292752,301.507828 134.22469,301.507828 C110.163898,301.507828 102.642899,313.596301 82.7151126,314.379442 C62.0350407,315.16201 46.2873831,293.668525 33.0744079,274.586162 C6.07529317,235.552544 -14.5576169,164.286328 13.147166,116.18047 C26.9103111,92.2909053 51.5060917,77.1630356 78.2026125,76.7751096 C98.5099145,76.3877456 117.677594,90.4371851 130.091705,90.4371851 C142.497945,90.4371851 165.790755,73.5415029 190.277627,76.0228474 C200.528668,76.4495055 229.303509,80.1636878 247.780625,107.209389 C246.291825,108.132333 213.44635,127.253405 213.803394,167.030988 M174.239142,50.1987033 C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 163.259413,63.4970262 174.239142,50.1987249" fill="#000000"></path></svg>'
)
);
this.matIconRegistry.addSvgIconLiteral(
'queues-list',
this.domSanitizer.bypassSecurityTrustHtml(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' +
'<path fill="#fff" d="M9 4V2H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h5v-2H4V4h5z"/>' +
'<path fill="#fff" d="M7 18V6h2v12H7zM11 6v12h2V6h-2zM15 20v2h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5v2h5v16h-5z"/>' +
'<path fill="#fff" d="M15 18V6h2v12h-2z"/>' +
'</svg>'
)
);
for (const svgIcon of Object.keys(svgIcons)) {
this.matIconRegistry.addSvgIconLiteral(
svgIcon,
this.domSanitizer.bypassSecurityTrustHtml(
svgIcons[svgIcon]
)
);
}
this.storageService.testLocalStorage();

2
ui-ngx/src/app/core/services/menu.models.ts

@ -24,7 +24,6 @@ export interface MenuSection extends HasUUID{
type: MenuSectionType;
path: string;
icon: string;
isMdiIcon?: boolean;
pages?: Array<MenuSection>;
opened?: boolean;
disabled?: boolean;
@ -39,6 +38,5 @@ export interface HomeSection {
export interface HomeSectionPlace {
name: string;
icon: string;
isMdiIcon?: boolean;
path: string;
}

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

@ -111,8 +111,7 @@ export class MenuService {
name: 'tenant-profile.tenant-profiles',
type: 'link',
path: '/tenantProfiles',
icon: 'mdi:alpha-t-box',
isMdiIcon: true
icon: 'mdi:alpha-t-box'
},
{
id: 'resources',
@ -133,8 +132,7 @@ export class MenuService {
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split',
isMdiIcon: true
icon: 'mdi:rhombus-split'
}
]
},
@ -144,7 +142,6 @@ export class MenuService {
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: 'notification_inbox',
@ -176,8 +173,7 @@ export class MenuService {
fullName: 'notification.notification-templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw',
isMdiIcon: true
icon: 'mdi:message-draw'
},
{
id: 'notification_rules',
@ -185,8 +181,7 @@ export class MenuService {
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog',
isMdiIcon: true
icon: 'mdi:message-cog'
}
]
},
@ -218,8 +213,7 @@ export class MenuService {
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge',
isMdiIcon: true
icon: 'mdi:message-badge'
},
{
id: 'queues',
@ -250,16 +244,14 @@ export class MenuService {
name: 'admin.2fa.2fa',
type: 'link',
path: '/security-settings/2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true
icon: 'mdi:two-factor-authentication'
},
{
id: 'oauth2',
name: 'admin.oauth2.oauth2',
type: 'link',
path: '/security-settings/oauth2',
icon: 'mdi:shield-account',
isMdiIcon: true
icon: 'mdi:shield-account'
}
]
}
@ -281,7 +273,6 @@ export class MenuService {
{
name: 'tenant-profile.tenant-profiles',
icon: 'mdi:alpha-t-box',
isMdiIcon: true,
path: '/tenantProfiles'
},
]
@ -327,7 +318,6 @@ export class MenuService {
{
name: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true,
path: '/settings/2fa'
},
{
@ -361,8 +351,7 @@ export class MenuService {
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline',
isMdiIcon: true
icon: 'mdi:alert-outline'
},
{
id: 'dashboards',
@ -413,16 +402,14 @@ export class MenuService {
name: 'device-profile.device-profiles',
type: 'link',
path: '/profiles/deviceProfiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true
icon: 'mdi:alpha-d-box'
},
{
id: 'asset_profiles',
name: 'asset-profile.asset-profiles',
type: 'link',
path: '/profiles/assetProfiles',
icon: 'mdi:alpha-a-box',
isMdiIcon: true
icon: 'mdi:alpha-a-box'
}
]
},
@ -513,8 +500,7 @@ export class MenuService {
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split',
isMdiIcon: true
icon: 'mdi:rhombus-split'
}
]
},
@ -524,7 +510,6 @@ export class MenuService {
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: 'notification_inbox',
@ -556,8 +541,7 @@ export class MenuService {
fullName: 'notification.notification-templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw',
isMdiIcon: true
icon: 'mdi:message-draw'
},
{
id: 'notification_rules',
@ -565,8 +549,7 @@ export class MenuService {
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog',
isMdiIcon: true
icon: 'mdi:message-cog'
}
]
},
@ -598,8 +581,7 @@ export class MenuService {
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge',
isMdiIcon: true
icon: 'mdi:message-badge'
},
{
id: 'repository_settings',
@ -673,7 +655,6 @@ export class MenuService {
{
name: 'asset-profile.asset-profiles',
icon: 'mdi:alpha-a-box',
isMdiIcon: true,
path: '/profiles/assetProfiles'
}
]
@ -689,7 +670,6 @@ export class MenuService {
{
name: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box',
isMdiIcon: true,
path: '/profiles/deviceProfiles'
},
{
@ -814,8 +794,7 @@ export class MenuService {
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline',
isMdiIcon: true
icon: 'mdi:alert-outline'
},
{
id: 'dashboards',
@ -874,7 +853,6 @@ export class MenuService {
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
isMdiIcon: true,
pages: [
{
id: 'notification_inbox',

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -182,6 +182,7 @@ import * as ToggleHeaderComponent from '@shared/components/toggle-header.compone
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.component';
import * as MaterialIconsComponent from '@shared/components/material-icons.component';
import * as TbIconComponent from '@shared/components/icon.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -484,6 +485,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent,
'@shared/components/material-icons.component': MaterialIconsComponent,
'@shared/components/icon.component': TbIconComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent,

63
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<form [formGroup]="widgetFormGroup" style="width: 1200px;">
<form class="tb-add-widget-dialog" [formGroup]="widgetFormGroup">
<mat-toolbar color="primary">
<h2 translate>widget.add</h2>
<span fxFlex>: {{data.widgetInfo.widgetName}}</span>
@ -32,40 +32,37 @@
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div mat-dialog-content style="padding: 0;">
<fieldset [disabled]="isLoading$ | async" style="position: relative; height: 600px;">
<tb-widget-config
[aliasController]="aliasController"
[functionsOnly]="false"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
isAdd
formControlName="widgetConfig">
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
</fieldset>
<div mat-dialog-content>
<tb-widget-config class="tb-absolute-fill"
[aliasController]="aliasController"
[functionsOnly]="false"
[dashboard]="dashboard"
[widget]="widget"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
isAdd
formControlName="widgetConfig">
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
</div>
<div mat-dialog-actions fxLayoutAlign="space-between center">
<button mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()"
cdkFocusInitial>
{{ 'action.cancel' | translate }}
@ -74,14 +71,14 @@
<button *ngIf="!previewMode"
mat-button color="primary"
type="button"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid"
[disabled]="widgetFormGroup.invalid"
(click)="previewMode = true"
cdkFocusInitial>
{{ 'widget-config.preview' | translate }}
</button>
<button mat-raised-button color="primary"
(click)="add()"
[disabled]="(isLoading$ | async) || widgetFormGroup.invalid">
[disabled]="widgetFormGroup.invalid">
{{ 'action.add' | translate }}
</button>
</div>

29
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.scss

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../scss/constants';
.tb-add-widget-dialog {
.mat-mdc-dialog-content {
padding: 0;
position: relative;
}
@media #{$mat-gt-xs} {
width: 1200px;
.mat-mdc-dialog-content {
height: 600px;
}
}
}

7
ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts

@ -14,12 +14,12 @@
/// limitations under the License.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { Component, Inject, OnInit, SkipSelf, ViewEncapsulation } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { FormGroupDirective, NgForm, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { Widget, WidgetConfigMode, widgetTypesData } from '@shared/models/widget.models';
@ -41,7 +41,8 @@ export interface AddWidgetDialogData {
selector: 'tb-add-widget-dialog',
templateUrl: './add-widget-dialog.component.html',
providers: [/*{provide: ErrorStateMatcher, useExisting: AddWidgetDialogComponent}*/],
styleUrls: []
styleUrls: ['./add-widget-dialog.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogComponent, Widget>
implements OnInit, ErrorStateMatcher {

4
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html

@ -60,7 +60,9 @@
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config">
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
</tb-widget-preview>
</div>
</fieldset>

3
ui-ngx/src/app/modules/home/components/entity/entities-table.component.html

@ -86,8 +86,7 @@
matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event)">
<mat-icon *ngIf="actionDescriptor.isMdiIcon" [svgIcon]="actionDescriptor.icon"></mat-icon>
<mat-icon *ngIf="!actionDescriptor.isMdiIcon">{{actionDescriptor.icon}}</mat-icon>
<tb-icon>{{actionDescriptor.icon}}</tb-icon>
</button>
<button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()"
matTooltip="{{ 'action.refresh' | translate }}"

1
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -129,7 +129,6 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
this.headerActionDescriptors.push({
name: this.translate.instant('event.clear-filter'),
icon: 'mdi:filter-variant-remove',
isMdiIcon: true,
isEnabled: () => !isEqual(this.filterParams, {}),
onAction: ($event) => {
this.clearFiter($event);

3
ui-ngx/src/app/modules/home/components/router-tabs.component.html

@ -24,8 +24,7 @@
#rla="routerLinkActive"
mat-tab-link
[active]="rla.isActive">
<mat-icon *ngIf="!tab.isMdiIcon && tab.icon !== null" class="tb-mat-18">{{tab.icon}}</mat-icon>
<mat-icon *ngIf="tab.isMdiIcon && tab.icon !== null" [svgIcon]="tab.icon" class="tb-mat-18"></mat-icon>
<tb-icon *ngIf="tab.icon !== null" class="tb-mat-18">{{tab.icon}}</tb-icon>
<span>{{tab.name | translate}}</span>
</a>
</nav>

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

@ -26,7 +26,7 @@
line-height: 40px;
min-width: 200px;
border-bottom: none;
mat-icon {
.mat-icon {
margin-right: 8px;
margin-left: 0;
}

2
ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.html

@ -96,7 +96,7 @@
<ng-container matColumnDef="icon">
<mat-header-cell *matHeaderCellDef style="width: 40px"> {{ 'widget-config.action-icon' | translate }} </mat-header-cell>
<mat-cell *matCellDef="let action" class="tb-icon-cell" style="width: 40px">
<mat-icon>{{ action.icon }}</mat-icon>
<tb-icon>{{ action.icon }}</tb-icon>
</mat-cell>
</ng-container>
<ng-container matColumnDef="typeName">

8
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -38,6 +38,9 @@ import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widge
import {
AlarmsTableBasicConfigComponent
} from '@home/components/widget/config/basic/alarm/alarms-table-basic-config.component';
import {
ValueCardBasicConfigComponent
} from '@home/components/widget/config/basic/cards/value-card-basic-config.component';
@NgModule({
declarations: [
@ -47,6 +50,7 @@ import {
TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
],
@ -63,6 +67,7 @@ import {
TimeseriesTableBasicConfigComponent,
FlotBasicConfigComponent,
AlarmsTableBasicConfigComponent,
ValueCardBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
]
@ -75,5 +80,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetCo
'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent,
'tb-timeseries-table-basic-config': TimeseriesTableBasicConfigComponent,
'tb-flot-basic-config': FlotBasicConfigComponent,
'tb-alarms-table-basic-config': AlarmsTableBasicConfigComponent
'tb-alarms-table-basic-config': AlarmsTableBasicConfigComponent,
'tb-value-card-basic-config': ValueCardBasicConfigComponent
};

28
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts

@ -21,14 +21,15 @@ import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation, WidgetConfig,
datasourcesHasOnlyComparisonAggregation,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import { isUndefined } from '@core/utils';
import { getLabel, setLabel } from '@home/components/widget/config/widget-settings.models';
@Component({
selector: 'tb-simple-card-basic-config',
@ -67,7 +68,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.simpleCardWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
label: [this.getDataKeyLabel(configData.config.datasources), []],
label: [getLabel(configData.config.datasources), []],
labelPosition: [configData.config.settings?.labelPosition, []],
units: [configData.config.units, []],
decimals: [configData.config.decimals, []],
@ -83,7 +84,7 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources;
this.setDataKeyLabel(config.label, this.widgetConfig.config.datasources);
setLabel(config.label, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;
this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals;
@ -95,25 +96,6 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
return this.widgetConfig;
}
private getDataKeyLabel(datasources?: Datasource[]): string {
if (datasources && datasources.length) {
const dataKeys = datasources[0].dataKeys;
if (dataKeys && dataKeys.length) {
return dataKeys[0].label;
}
}
return '';
}
private setDataKeyLabel(label: string, datasources?: Datasource[]) {
if (datasources && datasources.length) {
const dataKeys = datasources[0].dataKeys;
if (dataKeys && dataKeys.length) {
dataKeys[0].label = label;
}
}
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {

104
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html

@ -0,0 +1,104 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="valueCardWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeyLabel
hideDataKeyColor
hideDataKeyUnits
hideDataKeyDecimals
formControlName="datasources">
</tb-datasources>
<div class="tb-form-panel">
<div class="tb-form-panel-title" translate>widget-config.appearance</div>
<tb-image-cards-select rowHeight="{{ horizontal ? '3:1' : '7:5' }}"
[cols]="horizontal ? 2 : 4"
[colsLtMd]="horizontal ? 1 : 2"
label="{{ 'widgets.value-card.layout' | translate }}" formControlName="layout">
<tb-image-cards-select-option *ngFor="let layout of valueCardLayouts"
[value]="layout"
[image]="valueCardLayoutImageMap.get(layout)">
{{ valueCardLayoutTranslationMap.get(layout) | translate }}
</tb-image-cards-select-option>
</tb-image-cards-select>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showLabel">
{{ 'widgets.value-card.label' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-font-settings formControlName="labelFont"
[previewText]="valueCardWidgetConfigForm.get('label').value">
</tb-font-settings>
<tb-color-settings formControlName="labelColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showIcon">
{{ 'widgets.value-card.icon' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="iconSize" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="iconSizeUnit"></tb-css-unit-select>
<tb-material-icon-select asBoxInput
[color]="valueCardWidgetConfigForm.get('iconColor').value?.color"
formControlName="icon">
</tb-material-icon-select>
<tb-color-settings formControlName="iconColor">
</tb-color-settings>
</div>
</div>
<div class="tb-form-row">
<div class="fixed-title-width" translate>widgets.value-card.value</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-unit-input fxFlex formControlName="units"></tb-unit-input>
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix translate>widget-config.decimals-suffix</div>
</mat-form-field>
<tb-font-settings formControlName="valueFont"
[previewText]="valuePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="valueColor">
</tb-color-settings>
</div>
</div>
</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide fixed-title-width" formControlName="showDate">
{{ 'widgets.value-card.date' | translate }}
</mat-slide-toggle>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px">
<tb-date-format-select fxFlex formControlName="dateFormat"></tb-date-format-select>
<tb-font-settings formControlName="dateFont"
[previewText]="datePreviewFn">
</tb-font-settings>
<tb-color-settings formControlName="dateColor">
</tb-color-settings>
</div>
</div>
</ng-container>

276
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.ts

@ -0,0 +1,276 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Injector } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
WidgetConfig,
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { getTimewindowConfig } from '@home/components/widget/config/timewindow-config-panel.component';
import { formatValue, isDefinedAndNotNull, isUndefined } from '@core/utils';
import {
DateFormatProcessor,
DateFormatSettings,
getLabel,
setLabel
} from '@home/components/widget/config/widget-settings.models';
import {
valueCardDefaultSettings,
ValueCardLayout,
valueCardLayoutImages,
valueCardLayouts,
valueCardLayoutTranslations,
ValueCardWidgetSettings
} from '@home/components/widget/lib/cards/value-card-widget.models';
@Component({
selector: 'tb-value-card-basic-config',
templateUrl: './value-card-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class ValueCardBasicConfigComponent extends BasicWidgetConfigComponent {
public get displayTimewindowConfig(): boolean {
const datasources = this.valueCardWidgetConfigForm.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
public onlyHistoryTimewindow(): boolean {
const datasources = this.valueCardWidgetConfigForm.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
}
valueCardLayouts: ValueCardLayout[] = [];
valueCardLayoutTranslationMap = valueCardLayoutTranslations;
valueCardLayoutImageMap = valueCardLayoutImages;
horizontal = false;
valueCardWidgetConfigForm: UntypedFormGroup;
valuePreviewFn = this._valuePreviewFn.bind(this);
datePreviewFn = this._datePreviewFn.bind(this);
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private cd: ChangeDetectorRef,
private $injector: Injector,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.valueCardWidgetConfigForm;
}
protected setupConfig(widgetConfig: WidgetConfigComponentData) {
const params = widgetConfig.typeParameters as any;
this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false;
this.valueCardLayouts = valueCardLayouts(this.horizontal);
super.setupConfig(widgetConfig);
}
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData, [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }]);
}
protected onConfigSet(configData: WidgetConfigComponentData) {
const settings: ValueCardWidgetSettings = {...valueCardDefaultSettings(this.horizontal), ...(configData.config.settings || {})};
this.valueCardWidgetConfigForm = this.fb.group({
timewindowConfig: [getTimewindowConfig(configData.config), []],
datasources: [configData.config.datasources, []],
layout: [settings.layout, []],
showLabel: [settings.showLabel, []],
label: [getLabel(configData.config.datasources), []],
labelFont: [settings.labelFont, []],
labelColor: [settings.labelColor, []],
showIcon: [settings.showIcon, []],
iconSize: [settings.iconSize, [Validators.min(0)]],
iconSizeUnit: [settings.iconSizeUnit, []],
icon: [settings.icon, []],
iconColor: [settings.iconColor, []],
units: [configData.config.units, []],
decimals: [configData.config.decimals, []],
valueFont: [settings.valueFont, []],
valueColor: [settings.valueColor, []],
showDate: [settings.showDate, []],
dateFormat: [settings.dateFormat, []],
dateFont: [settings.dateFont, []],
dateColor: [settings.dateColor, []],
background: [settings.background, []],
cardButtons: [this.getCardButtons(configData.config), []],
borderRadius: [configData.config.borderRadius, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.layout = config.layout;
this.widgetConfig.config.settings.showLabel = config.showLabel;
setLabel(config.label, this.widgetConfig.config.datasources);
this.widgetConfig.config.settings.labelFont = config.labelFont;
this.widgetConfig.config.settings.labelColor = config.labelColor;
this.widgetConfig.config.settings.showIcon = config.showIcon;
this.widgetConfig.config.settings.iconSize = config.iconSize;
this.widgetConfig.config.settings.iconSizeUnit = config.iconSizeUnit;
this.widgetConfig.config.settings.icon = config.icon;
this.widgetConfig.config.settings.iconColor = config.iconColor;
this.widgetConfig.config.units = config.units;
this.widgetConfig.config.decimals = config.decimals;
this.widgetConfig.config.settings.valueFont = config.valueFont;
this.widgetConfig.config.settings.valueColor = config.valueColor;
this.widgetConfig.config.settings.showDate = config.showDate;
this.widgetConfig.config.settings.dateFormat = config.dateFormat;
this.widgetConfig.config.settings.dateFont = config.dateFont;
this.widgetConfig.config.settings.dateColor = config.dateColor;
this.widgetConfig.config.settings.background = config.background;
this.setCardButtons(config.cardButtons, this.widgetConfig.config);
this.widgetConfig.config.borderRadius = config.borderRadius;
this.widgetConfig.config.actions = config.actions;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['layout', 'showLabel', 'showIcon', 'showDate'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const layout: ValueCardLayout = this.valueCardWidgetConfigForm.get('layout').value;
const showLabel: boolean = this.valueCardWidgetConfigForm.get('showLabel').value;
const showIcon: boolean = this.valueCardWidgetConfigForm.get('showIcon').value;
const showDate: boolean = this.valueCardWidgetConfigForm.get('showDate').value;
const dateEnabled = ![ValueCardLayout.vertical, ValueCardLayout.simplified].includes(layout);
const iconEnabled = layout !== ValueCardLayout.simplified;
if (showLabel) {
this.valueCardWidgetConfigForm.get('label').enable();
this.valueCardWidgetConfigForm.get('labelFont').enable();
this.valueCardWidgetConfigForm.get('labelColor').enable();
} else {
this.valueCardWidgetConfigForm.get('label').disable();
this.valueCardWidgetConfigForm.get('labelFont').disable();
this.valueCardWidgetConfigForm.get('labelColor').disable();
}
if (iconEnabled) {
this.valueCardWidgetConfigForm.get('showIcon').enable({emitEvent: false});
if (showIcon) {
this.valueCardWidgetConfigForm.get('iconSize').enable();
this.valueCardWidgetConfigForm.get('iconSizeUnit').enable();
this.valueCardWidgetConfigForm.get('icon').enable();
this.valueCardWidgetConfigForm.get('iconColor').enable();
} else {
this.valueCardWidgetConfigForm.get('iconSize').disable();
this.valueCardWidgetConfigForm.get('iconSizeUnit').disable();
this.valueCardWidgetConfigForm.get('icon').disable();
this.valueCardWidgetConfigForm.get('iconColor').disable();
}
} else {
this.valueCardWidgetConfigForm.get('showIcon').disable({emitEvent: false});
this.valueCardWidgetConfigForm.get('iconSize').disable();
this.valueCardWidgetConfigForm.get('iconSizeUnit').disable();
this.valueCardWidgetConfigForm.get('icon').disable();
this.valueCardWidgetConfigForm.get('iconColor').disable();
}
if (dateEnabled) {
this.valueCardWidgetConfigForm.get('showDate').enable({emitEvent: false});
if (showDate) {
this.valueCardWidgetConfigForm.get('dateFormat').enable();
this.valueCardWidgetConfigForm.get('dateFont').enable();
this.valueCardWidgetConfigForm.get('dateColor').enable();
} else {
this.valueCardWidgetConfigForm.get('dateFormat').disable();
this.valueCardWidgetConfigForm.get('dateFont').disable();
this.valueCardWidgetConfigForm.get('dateColor').disable();
}
} else {
this.valueCardWidgetConfigForm.get('showDate').disable({emitEvent: false});
this.valueCardWidgetConfigForm.get('dateFormat').disable();
this.valueCardWidgetConfigForm.get('dateFont').disable();
this.valueCardWidgetConfigForm.get('dateColor').disable();
}
this.valueCardWidgetConfigForm.get('showIcon').updateValueAndValidity({emitEvent: false});
this.valueCardWidgetConfigForm.get('showDate').updateValueAndValidity({emitEvent: false});
this.valueCardWidgetConfigForm.get('label').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('labelFont').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('labelColor').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('iconSize').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('iconSizeUnit').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('icon').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('dateFormat').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('dateFont').updateValueAndValidity({emitEvent});
this.valueCardWidgetConfigForm.get('dateColor').updateValueAndValidity({emitEvent});
}
private getCardButtons(config: WidgetConfig): string[] {
const buttons: string[] = [];
if (isUndefined(config.enableFullscreen) || config.enableFullscreen) {
buttons.push('fullscreen');
}
return buttons;
}
private setCardButtons(buttons: string[], config: WidgetConfig) {
config.enableFullscreen = buttons.includes('fullscreen');
}
private _valuePreviewFn(): string {
const units: string = this.valueCardWidgetConfigForm.get('units').value;
const decimals: number = this.valueCardWidgetConfigForm.get('decimals').value;
return formatValue(22, decimals, units, true);
}
private _datePreviewFn(): string {
const dateFormat: DateFormatSettings = this.valueCardWidgetConfigForm.get('dateFormat').value;
const processor = DateFormatProcessor.fromSettings(this.$injector, dateFormat);
processor.update(Date.now());
return processor.formatted;
}
}

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.html

@ -21,7 +21,7 @@
<mat-chip-listbox fxFlex>
<ng-container *ngFor="let actionSourceId of widgetActionSourceIds">
<mat-chip *ngFor="let widgetAction of widgetActionsByActionSourceId(actionSourceId)">
<mat-icon matChipAvatar>{{ widgetAction.icon }}</mat-icon>
<tb-icon matChipAvatar>{{ widgetAction.icon }}</tb-icon>
{{ widgetAction.name }}
</mat-chip>
</ng-container>

2
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.html

@ -73,7 +73,7 @@
</div>
<div *ngIf="!hideDataKeyColor" style="padding: 3px;">
<div #keyColorButton class="tb-color-preview small box" (click)="openColorPickerPopup(key, $event, keyColorButton)">
<div class="tb-color-result" [ngStyle]="{background: key.color}"></div>
<div class="tb-color-result" [style]="{background: key.color}"></div>
</div>
</div>
<button *ngIf="!disabled"

365
ui-ngx/src/app/modules/home/components/widget/config/widget-settings.models.ts

@ -0,0 +1,365 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { isDefinedAndNotNull, isNumber, isNumeric, parseFunction } from '@core/utils';
import { DataKey, Datasource, DatasourceData } from '@shared/models/widget.models';
import { Injector } from '@angular/core';
import { DatePipe, formatDate } from '@angular/common';
import { DateAgoPipe } from '@shared/pipe/date-ago.pipe';
import { TranslateService } from '@ngx-translate/core';
export type ComponentStyle = {[klass: string]: any};
export const cssUnits = ['px', 'em', '%', 'rem', 'pt', 'pc', 'in', 'cm', 'mm', 'ex', 'ch', 'vw', 'vh', 'vmin', 'vmax'] as const;
type cssUnitTuple = typeof cssUnits;
export type cssUnit = cssUnitTuple[number];
export const fontWeights = ['normal', 'bold', 'bolder', 'lighter', '100', '200', '300', '400', '500', '600', '700', '800', '900'] as const;
type fontWeightTuple = typeof fontWeights;
export type fontWeight = fontWeightTuple[number];
export const fontWeightTranslations = new Map<fontWeight, string>(
[
['normal', 'widgets.widget-font.font-weight-normal'],
['bold', 'widgets.widget-font.font-weight-bold'],
['bolder', 'widgets.widget-font.font-weight-bolder'],
['lighter', 'widgets.widget-font.font-weight-lighter']
]
);
export const fontStyles = ['normal', 'italic', 'oblique'] as const;
type fontStyleTuple = typeof fontStyles;
export type fontStyle = fontStyleTuple[number];
export const fontStyleTranslations = new Map<fontStyle, string>(
[
['normal', 'widgets.widget-font.font-style-normal'],
['italic', 'widgets.widget-font.font-style-italic'],
['oblique', 'widgets.widget-font.font-style-oblique']
]
);
export const commonFonts = ['Roboto', 'monospace', 'sans-serif', 'serif'];
export interface Font {
size: number;
sizeUnit: cssUnit;
family: string;
weight: fontWeight;
style: fontStyle;
}
export enum ColorType {
constant = 'constant',
range = 'range',
function = 'function'
}
export const colorTypeTranslations = new Map<ColorType, string>(
[
[ColorType.constant, 'widgets.color.color-type-constant'],
[ColorType.range, 'widgets.color.color-type-range'],
[ColorType.function, 'widgets.color.color-type-function']
]
);
export interface ColorRange {
from?: number;
to?: number;
color: string;
}
export interface ColorSettings {
type: ColorType;
color: string;
rangeList?: ColorRange[];
colorFunction?: string;
}
export const constantColor = (color: string): ColorSettings => ({
type: ColorType.constant,
color,
colorFunction: 'var temperature = value;\n' +
'if (typeof temperature !== undefined) {\n' +
' var percent = (temperature + 60)/120 * 100;\n' +
' return tinycolor.mix(\'blue\', \'red\', percent).toHexString();\n' +
'}\n' +
'return \'blue\';'
});
type ValueColorFunction = (value: any) => string;
export abstract class ColorProcessor {
static fromSettings(color: ColorSettings): ColorProcessor {
switch (color.type) {
case ColorType.constant:
return new ConstantColorProcessor(color);
case ColorType.range:
return new RangeColorProcessor(color);
case ColorType.function:
return new FunctionColorProcessor(color);
}
}
color: string;
protected constructor(protected settings: ColorSettings) {
this.color = settings.color;
}
abstract update(value: any): void;
}
class ConstantColorProcessor extends ColorProcessor {
constructor(protected settings: ColorSettings) {
super(settings);
}
update(value: any): void {}
}
class RangeColorProcessor extends ColorProcessor {
constructor(protected settings: ColorSettings) {
super(settings);
}
update(value: any): void {
this.color = this.computeFromRange(value);
}
private computeFromRange(value: any): string {
if (this.settings.rangeList?.length && isDefinedAndNotNull(value) && isNumeric(value)) {
const num = Number(value);
for (const range of this.settings.rangeList) {
if ((!isNumber(range.from) || num >= range.from) && (!isNumber(range.to) || num < range.to)) {
return range.color;
}
}
}
return this.settings.color;
}
}
class FunctionColorProcessor extends ColorProcessor {
private readonly colorFunction: ValueColorFunction;
constructor(protected settings: ColorSettings) {
super(settings);
this.colorFunction = parseFunction(settings.colorFunction, ['value']);
}
update(value: any): void {
if (this.colorFunction) {
this.color = this.colorFunction(value) || this.settings.color;
}
}
}
export interface DateFormatSettings {
format?: string;
lastUpdateAgo?: boolean;
custom?: boolean;
}
export const simpleDateFormat = (format: string): DateFormatSettings => ({
format,
lastUpdateAgo: false,
custom: false
});
export const lastUpdateAgoDateFormat = (): DateFormatSettings => ({
format: null,
lastUpdateAgo: true,
custom: false
});
export const customDateFormat = (format: string): DateFormatSettings => ({
format,
lastUpdateAgo: false,
custom: true
});
export const dateFormats = ['MMM dd yyyy HH:mm', 'dd MMM yyyy HH:mm', 'yyyy MMM dd HH:mm',
'MM/dd/yyyy HH:mm', 'dd/MM/yyyy HH:mm', 'yyyy/MM/dd HH:mm:ss']
.map(f => simpleDateFormat(f)).concat([lastUpdateAgoDateFormat(), customDateFormat('EEE, MMMM dd, yyyy')]);
export const compareDateFormats = (df1: DateFormatSettings, df2: DateFormatSettings): boolean => {
if (df1 === df2) {
return true;
} else if (df1 && df2) {
if (df1.lastUpdateAgo && df2.lastUpdateAgo) {
return true;
} else if (df1.custom && df2.custom) {
return true;
} else if (!df1.lastUpdateAgo && !df2.lastUpdateAgo && !df1.custom && !df2.custom) {
return df1.format === df2.format;
}
}
return false;
};
export abstract class DateFormatProcessor {
static fromSettings($injector: Injector, settings: DateFormatSettings): DateFormatProcessor {
if (settings.lastUpdateAgo) {
return new LastUpdateAgoDateFormatProcessor($injector, settings);
} else {
return new SimpleDateFormatProcessor($injector, settings);
}
}
formatted = '';
protected constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
}
abstract update(ts: string | number | Date): void;
}
export class SimpleDateFormatProcessor extends DateFormatProcessor {
private datePipe: DatePipe;
constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
super($injector, settings);
this.datePipe = $injector.get(DatePipe);
}
update(ts: string| number | Date): void {
this.formatted = this.datePipe.transform(ts, this.settings.format);
}
}
export class LastUpdateAgoDateFormatProcessor extends DateFormatProcessor {
private dateAgoPipe: DateAgoPipe;
private translate: TranslateService;
constructor(protected $injector: Injector,
protected settings: DateFormatSettings) {
super($injector, settings);
this.dateAgoPipe = $injector.get(DateAgoPipe);
this.translate = $injector.get(TranslateService);
}
update(ts: string| number | Date): void {
this.formatted = this.translate.instant('date.last-update-n-ago-text',
{agoText: this.dateAgoPipe.transform(ts, {applyAgo: true, short: true, textPart: true})});
}
}
export enum BackgroundType {
image = 'image',
imageUrl = 'imageUrl',
color = 'color'
}
export interface OverlaySettings {
enabled: boolean;
color: string;
blur: number;
}
export interface BackgroundSettings {
type: BackgroundType;
imageBase64?: string;
imageUrl?: string;
color?: string;
overlay: OverlaySettings;
}
export const iconStyle = (size: number, sizeUnit: cssUnit): ComponentStyle => {
const iconSize = size + sizeUnit;
return {
width: iconSize,
height: iconSize,
fontSize: iconSize,
lineHeight: iconSize
};
};
export const textStyle = (font: Font, lineHeight = '1.5', letterSpacing = '0.25px'): ComponentStyle => ({
font: font.style + ' normal ' + font.weight + ' ' + (font.size+font.sizeUnit) + '/' + lineHeight + ' ' + font.family +
(font.family !== 'Roboto' ? ', Roboto' : ''),
letterSpacing
});
export const backgroundStyle = (background: BackgroundSettings): ComponentStyle => {
if (background.type === BackgroundType.color) {
return {
background: background.color
};
} else {
const imageUrl = background.type === BackgroundType.image ? background.imageBase64 : background.imageUrl;
return {
background: `url(${imageUrl}) no-repeat`,
backgroundSize: 'cover',
backgroundPosition: '50% 50%'
};
}
};
export const overlayStyle = (overlay: OverlaySettings): ComponentStyle => (
{
display: overlay.enabled ? 'block' : 'none',
background: overlay.color,
backdropFilter: `blur(${overlay.blur}px)`
}
);
export const getDataKey = (datasources?: Datasource[]): DataKey => {
if (datasources && datasources.length) {
const dataKeys = datasources[0].dataKeys;
if (dataKeys && dataKeys.length) {
return dataKeys[0];
}
}
return null;
};
export const getLabel = (datasources?: Datasource[]): string => {
const dataKey = getDataKey(datasources);
if (dataKey) {
return dataKey.label;
}
return '';
};
export const setLabel = (label: string, datasources?: Datasource[]): void => {
const dataKey = getDataKey(datasources);
if (dataKey) {
dataKey.label = label;
}
};
export const getSingleTsValue = (data: Array<DatasourceData>): [number, any] => {
if (data.length) {
const dsData = data[0];
if (dsData.data.length) {
return dsData.data[0];
}
}
return null;
};

70
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.html

@ -0,0 +1,70 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-value-card-panel" [class]="this.layout" [style]="backgroundStyle">
<div class="tb-value-card-overlay" [style]="overlayStyle"></div>
<ng-container [ngSwitch]="layout">
<ng-template [ngSwitchCase]="valueCardLayout.square">
<ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.vertical">
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
<ng-container *ngTemplateOutlet="iconTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.centered">
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<div class="tb-value-card-icon-row">
<ng-container *ngTemplateOutlet="iconTpl"></ng-container>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</div>
<ng-container *ngTemplateOutlet="dateTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="valueCardLayout.simplified">
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
</ng-template>
<ng-template [ngSwitchCase]="layout === valueCardLayout.horizontal ||
layout === valueCardLayout.horizontal_reversed ? layout : ''">
<ng-container *ngTemplateOutlet="iconWithLabelTpl"></ng-container>
<div fxFlex></div>
<ng-container *ngTemplateOutlet="valueTpl"></ng-container>
</ng-template>
</ng-container>
</div>
<ng-template #iconWithLabelTpl>
<div class="tb-value-card-icon-row">
<ng-container *ngTemplateOutlet="iconTpl"></ng-container>
<div class="tb-value-card-label-row">
<ng-container *ngTemplateOutlet="labelTpl"></ng-container>
<ng-container *ngTemplateOutlet="dateTpl"></ng-container>
</div>
</div>
</ng-template>
<ng-template #iconTpl>
<tb-icon *ngIf="showIcon" [style]="iconStyle" [style.color]="iconColor.color">{{ icon }}</tb-icon>
</ng-template>
<ng-template #labelTpl>
<div *ngIf="showLabel" [style]="labelStyle" [style.color]="labelColor.color">{{ label }}</div>
</ng-template>
<ng-template #dateTpl>
<div *ngIf="showDate" [style]="dateStyle" [style.color]="dateColor.color">{{ dateFormat.formatted }}</div>
</ng-template>
<ng-template #valueTpl>
<div class="tb-value-card-value" [style]="valueStyle" [style.color]="valueColor.color">{{ valueText }}</div>
</ng-template>

70
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.scss

@ -0,0 +1,70 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
.tb-value-card-panel {
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
padding: 24px;
> div:not(.tb-value-card-overlay) {
z-index: 1;
}
&.square {
justify-content: space-evenly;
gap: 0;
}
&.horizontal {
flex-direction: row;
}
&.horizontal_reversed {
flex-direction: row-reverse;
}
.tb-value-card-overlay {
position: absolute;
top: 12px;
left: 12px;
bottom: 12px;
right: 12px;
}
.tb-value-card-icon-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
&.horizontal_reversed {
.tb-value-card-icon-row {
flex-direction: row-reverse;
}
.tb-value-card-label-row {
align-items: flex-end;
}
}
.tb-value-card-label-row {
display: flex;
flex-direction: column;
justify-content: center;
}
.tb-value-card-value {
white-space: nowrap;
}
}
}

144
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.component.ts

@ -0,0 +1,144 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { WidgetContext } from '@home/models/widget-component.models';
import { formatValue, isDefinedAndNotNull } from '@core/utils';
import { DatePipe } from '@angular/common';
import {
backgroundStyle,
ColorProcessor,
ComponentStyle, DateFormatProcessor,
getDataKey,
getLabel,
getSingleTsValue,
iconStyle,
overlayStyle,
textStyle
} from '@home/components/widget/config/widget-settings.models';
import { valueCardDefaultSettings, ValueCardLayout, ValueCardWidgetSettings } from './value-card-widget.models';
import { WidgetComponent } from '@home/components/widget/widget.component';
@Component({
selector: 'tb-value-card-widget',
templateUrl: './value-card-widget.component.html',
styleUrls: ['./value-card-widget.component.scss']
})
export class ValueCardWidgetComponent implements OnInit {
settings: ValueCardWidgetSettings;
valueCardLayout = ValueCardLayout;
@Input()
ctx: WidgetContext;
layout: ValueCardLayout;
showIcon = true;
icon = '';
iconStyle: ComponentStyle = {};
iconColor: ColorProcessor;
showLabel = true;
label = '';
labelStyle: ComponentStyle = {};
labelColor: ColorProcessor;
valueText = 'N/A';
valueStyle: ComponentStyle = {};
valueColor: ColorProcessor;
showDate = true;
dateFormat: DateFormatProcessor;
dateStyle: ComponentStyle = {};
dateColor: ColorProcessor;
backgroundStyle: ComponentStyle = {};
overlayStyle: ComponentStyle = {};
private horizontal = false;
private decimals = 0;
private units = '';
constructor(private date: DatePipe,
private widgetComponent: WidgetComponent,
private cd: ChangeDetectorRef) {
}
ngOnInit(): void {
const params = this.widgetComponent.typeParameters as any;
this.horizontal = isDefinedAndNotNull(params.horizontal) ? params.horizontal : false;
this.ctx.$scope.valueCardWidget = this;
this.settings = {...valueCardDefaultSettings(this.horizontal), ...this.ctx.settings};
this.decimals = this.ctx.decimals;
this.units = this.ctx.units;
const dataKey = getDataKey(this.ctx.datasources);
if (isDefinedAndNotNull(dataKey?.decimals)) {
this.decimals = dataKey.decimals;
}
if (dataKey?.units) {
this.units = dataKey.units;
}
this.layout = this.settings.layout;
this.showIcon = this.settings.showIcon;
this.icon = this.settings.icon;
this.iconStyle = iconStyle(this.settings.iconSize, this.settings.iconSizeUnit );
this.iconColor = ColorProcessor.fromSettings(this.settings.iconColor);
this.showLabel = this.settings.showLabel;
this.label = getLabel(this.ctx.datasources);
this.labelStyle = textStyle(this.settings.labelFont, '1.5', '0.25px');
this.labelColor = ColorProcessor.fromSettings(this.settings.labelColor);
this.valueStyle = textStyle(this.settings.valueFont, '100%', '0.13px');
this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
this.showDate = this.settings.showDate;
this.dateFormat = DateFormatProcessor.fromSettings(this.ctx.$injector, this.settings.dateFormat);
this.dateStyle = textStyle(this.settings.dateFont, '1.33', '0.25px');
this.dateColor = ColorProcessor.fromSettings(this.settings.dateColor);
this.backgroundStyle = backgroundStyle(this.settings.background);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
}
public onInit() {
const borderRadius = this.ctx.$widgetElement.css('borderRadius');
this.overlayStyle = {...this.overlayStyle, ...{borderRadius}};
this.cd.detectChanges();
}
public onDataUpdated() {
const tsValue = getSingleTsValue(this.ctx.data);
let ts;
let value;
if (tsValue) {
ts = tsValue[0];
value = tsValue[1];
this.valueText = formatValue(value, this.decimals, this.units, true);
} else {
this.valueText = 'N/A';
}
this.dateFormat.update(ts);
this.iconColor.update(value);
this.labelColor.update(value);
this.valueColor.update(value);
this.dateColor.update(value);
this.cd.detectChanges();
}
}

127
ui-ngx/src/app/modules/home/components/widget/lib/cards/value-card-widget.models.ts

@ -0,0 +1,127 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
BackgroundSettings,
BackgroundType,
ColorSettings,
constantColor,
cssUnit, DateFormatSettings,
Font, lastUpdateAgoDateFormat
} from '@home/components/widget/config/widget-settings.models';
export enum ValueCardLayout {
square = 'square',
vertical = 'vertical',
centered = 'centered',
simplified = 'simplified',
horizontal = 'horizontal',
horizontal_reversed = 'horizontal_reversed'
}
export const valueCardLayouts = (horizontal: boolean): ValueCardLayout[] => {
if (horizontal) {
return [ValueCardLayout.horizontal, ValueCardLayout.horizontal_reversed];
} else {
return [ValueCardLayout.square, ValueCardLayout.vertical, ValueCardLayout.centered, ValueCardLayout.simplified];
}
};
export const valueCardLayoutTranslations = new Map<ValueCardLayout, string>(
[
[ValueCardLayout.square, 'widgets.value-card.layout-square'],
[ValueCardLayout.vertical, 'widgets.value-card.layout-vertical'],
[ValueCardLayout.centered, 'widgets.value-card.layout-centered'],
[ValueCardLayout.simplified, 'widgets.value-card.layout-simplified'],
[ValueCardLayout.horizontal, 'widgets.value-card.layout-horizontal'],
[ValueCardLayout.horizontal_reversed, 'widgets.value-card.layout-horizontal-reversed']
]
);
export const valueCardLayoutImages = new Map<ValueCardLayout, string>(
[
[ValueCardLayout.square, 'assets/widget/value-card/square-layout.svg'],
[ValueCardLayout.vertical, 'assets/widget/value-card/vertical-layout.svg'],
[ValueCardLayout.centered, 'assets/widget/value-card/centered-layout.svg'],
[ValueCardLayout.simplified, 'assets/widget/value-card/simplified-layout.svg'],
[ValueCardLayout.horizontal, 'assets/widget/value-card/horizontal-layout.svg'],
[ValueCardLayout.horizontal_reversed, 'assets/widget/value-card/horizontal-reversed-layout.svg']
]
);
export interface ValueCardWidgetSettings {
layout: ValueCardLayout;
showLabel: boolean;
labelFont: Font;
labelColor: ColorSettings;
showIcon: boolean;
icon: string;
iconSize: number;
iconSizeUnit: cssUnit;
iconColor: ColorSettings;
valueFont: Font;
valueColor: ColorSettings;
showDate: boolean;
dateFormat: DateFormatSettings;
dateFont: Font;
dateColor: ColorSettings;
background: BackgroundSettings;
}
export const valueCardDefaultSettings = (horizontal: boolean): ValueCardWidgetSettings => ({
layout: horizontal ? ValueCardLayout.horizontal : ValueCardLayout.square,
showLabel: true,
labelFont: {
family: 'Roboto',
size: 16,
sizeUnit: 'px',
style: 'normal',
weight: '500'
},
labelColor: constantColor('rgba(0, 0, 0, 0.87)'),
showIcon: true,
icon: 'thermostat',
iconSize: 40,
iconSizeUnit: 'px',
iconColor: constantColor('#5469FF'),
valueFont: {
family: 'Roboto',
size: 52,
sizeUnit: 'px',
style: 'normal',
weight: '500'
},
valueColor: constantColor('rgba(0, 0, 0, 0.87)'),
showDate: true,
dateFormat: lastUpdateAgoDateFormat(),
dateFont: {
family: 'Roboto',
size: 12,
sizeUnit: 'px',
style: 'normal',
weight: '500'
},
dateColor: constantColor('rgba(0, 0, 0, 0.38)'),
background: {
type: BackgroundType.color,
color: '#fff',
overlay: {
enabled: false,
color: 'rgba(255,255,255,0.72)',
blur: 3
}
}
});

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-doc-link-dialog.component.html

@ -22,7 +22,7 @@
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
<tb-icon>close</tb-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>

2
ui-ngx/src/app/modules/home/components/widget/lib/home-page/add-quick-link-dialog.component.html

@ -22,7 +22,7 @@
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
<tb-icon>close</tb-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>

10
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-link.component.html

@ -38,8 +38,8 @@
</div>
</div>
<div *ngIf="editMode" fxLayout="row" class="tb-edit-buttons">
<button mat-icon-button (click)="apply()"><mat-icon>check</mat-icon></button>
<button mat-icon-button (click)="cancelEdit()"><mat-icon>close</mat-icon></button>
<button mat-icon-button (click)="apply()"><tb-icon>check</tb-icon></button>
<button mat-icon-button (click)="cancelEdit()"><tb-icon>close</tb-icon></button>
</div>
</div>
<ng-template #docLinkTemplate>
@ -47,7 +47,7 @@
<div fxFlex class="tb-link">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon color="primary">{{ docLink.icon }}</mat-icon>
<tb-icon color="primary">{{ docLink.icon }}</tb-icon>
</div>
<div class="tb-link-text">{{ docLink.name }}</div>
</div>
@ -57,13 +57,13 @@
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="switchToEditMode()">
<mat-icon>edit</mat-icon>
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="delete()">
<mat-icon>delete</mat-icon>
<tb-icon>delete</tb-icon>
</button>
</div>
</div>

6
ui-ngx/src/app/modules/home/components/widget/lib/home-page/doc-links-widget.component.html

@ -23,7 +23,7 @@
matTooltipPosition="above"
mat-icon-button
(click)="edit()">
<mat-icon>edit</mat-icon>
<tb-icon>edit</tb-icon>
</button>
</div>
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
@ -32,7 +32,7 @@
[href]="docLink.link" target="_blank">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon color="primary">{{ docLink.icon }}</mat-icon>
<tb-icon color="primary">{{ docLink.icon }}</tb-icon>
</div>
<div class="tb-link-text">{{ docLink.name }}</div>
</div>
@ -43,7 +43,7 @@
matTooltip="{{ 'widgets.documentation.add-link' | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
<tb-icon class="tb-add-icon">add</tb-icon>
</div>
</mat-grid-tile>
</mat-grid-list>

6
ui-ngx/src/app/modules/home/components/widget/lib/home-page/edit-links-dialog.component.html

@ -22,7 +22,7 @@
<button mat-icon-button
(click)="close()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
<tb-icon class="material-icons">close</tb-icon>
</button>
</mat-toolbar>
<div mat-dialog-content>
@ -60,7 +60,7 @@
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above"
class="tb-drag-handle">
<mat-icon>drag_indicator</mat-icon>
<tb-icon>drag_indicator</tb-icon>
</div>
</ng-template>
</div>
@ -71,7 +71,7 @@
matTooltip="{{ (mode === 'docs' ? 'widgets.documentation.add-link' : 'widgets.quick-links.add-link') | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
<tb-icon class="tb-add-icon">add</tb-icon>
</div>
<ng-container *ngIf="addMode">
<ng-container [ngSwitch]="mode">

1
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page.scss

@ -23,6 +23,7 @@
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
.mat-icon {
vertical-align: bottom;
margin-right: 10px;
color: rgba(0, 0, 0, 0.54);
font-size: 20px;

19
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.html

@ -25,21 +25,19 @@
(focusin)="onFocus()"
required
[matAutocomplete]="linkAutocomplete">
<mat-icon matPrefix *ngIf="quickLink && !quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon>
<mat-icon matPrefix *ngIf="quickLink && quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon>
<tb-icon matPrefix *ngIf="quickLink" color="primary">{{ quickLink.icon }}</tb-icon>
<button *ngIf="editQuickLinkFormGroup.get('link').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
<tb-icon>close</tb-icon>
</button>
<mat-autocomplete
class="tb-autocomplete tb-quick-links"
#linkAutocomplete="matAutocomplete"
[displayWith]="displayLinkFn">
<mat-option *ngFor="let link of filteredLinks | async" [value]="link">
<mat-icon *ngIf="!link.isMdiIcon">{{ link.icon }}</mat-icon>
<mat-icon *ngIf="link.isMdiIcon" [svgIcon]="link.icon"></mat-icon>
<tb-icon>{{ link.icon }}</tb-icon>
<span [innerHTML]="link.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredLinks | async)?.length" [value]="null">
@ -58,8 +56,8 @@
</div>
</div>
<div *ngIf="editMode" fxLayout="row" class="tb-edit-buttons">
<button mat-icon-button (click)="apply()"><mat-icon>check</mat-icon></button>
<button mat-icon-button (click)="cancelEdit()"><mat-icon>close</mat-icon></button>
<button mat-icon-button (click)="apply()"><tb-icon>check</tb-icon></button>
<button mat-icon-button (click)="cancelEdit()"><tb-icon>close</tb-icon></button>
</div>
</div>
<ng-template #quickLinkTemplate>
@ -67,8 +65,7 @@
<div fxFlex class="tb-link">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon *ngIf="!quickLink?.isMdiIcon" color="primary">{{ quickLink?.icon }}</mat-icon>
<mat-icon *ngIf="quickLink?.isMdiIcon" color="primary" [svgIcon]="quickLink?.icon"></mat-icon>
<tb-icon color="primary">{{ quickLink?.icon }}</tb-icon>
</div>
<div class="tb-link-text">{{ displayLinkFn(quickLink) }}</div>
</div>
@ -78,13 +75,13 @@
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="switchToEditMode()">
<mat-icon>edit</mat-icon>
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="delete()">
<mat-icon>delete</mat-icon>
<tb-icon>delete</tb-icon>
</button>
</div>
</div>

7
ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-links-widget.component.html

@ -23,7 +23,7 @@
matTooltipPosition="above"
mat-icon-button
(click)="edit()">
<mat-icon>edit</mat-icon>
<tb-icon>edit</tb-icon>
</button>
</div>
<mat-grid-list class="tb-links-list" fxFlex [cols]="columns" [rowHeight]="rowHeight" [gutterSize]="gutterSize">
@ -32,8 +32,7 @@
[routerLink]="quickLink.path">
<div class="tb-link-container">
<div class="tb-link-icon-container">
<mat-icon *ngIf="!quickLink.isMdiIcon" color="primary">{{ quickLink.icon }}</mat-icon>
<mat-icon *ngIf="quickLink.isMdiIcon" color="primary" [svgIcon]="quickLink.icon"></mat-icon>
<tb-icon color="primary">{{ quickLink.icon }}</tb-icon>
</div>
<div class="tb-link-text">{{ (quickLink.fullName || quickLink.name) | translate }}</div>
</div>
@ -44,7 +43,7 @@
matTooltip="{{ 'widgets.quick-links.add-link' | translate }}"
matTooltipPosition="above"
(click)="addLink()">
<mat-icon class="tb-add-icon">add</mat-icon>
<tb-icon class="tb-add-icon">add</tb-icon>
</div>
</mat-grid-tile>
</mat-grid-list>

8
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html

@ -38,7 +38,7 @@
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
@ -63,7 +63,7 @@
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
@ -99,7 +99,7 @@
(blur)="key.isFocused = false; inputChanged(source, key)"
/>
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
@ -123,7 +123,7 @@
[labelPosition]="key.settings.slideToggleLabelPosition"
(change)="inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>

2
ui-ngx/src/app/modules/home/components/widget/lib/navigation-card-widget.component.html

@ -16,6 +16,6 @@
-->
<a mat-raised-button color="primary" class="tb-nav-button" href="{{settings.path}}" (click)="navigate($event, settings.path)">
<mat-icon class="material-icons tb-mat-96">{{settings.icon}}</mat-icon>
<tb-icon class="tb-mat-96">{{settings.icon}}</tb-icon>
<span>{{translatedName}}</span>
</a>

3
ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html

@ -25,8 +25,7 @@
<mat-grid-list rowHeight="170px" [cols]="sectionPlaces(section).length">
<mat-grid-tile *ngFor="let place of sectionPlaces(section)">
<a mat-raised-button color="primary" class="tb-card-button" href="{{place.path}}" (click)="navigate($event, place.path)">
<mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
<mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
<tb-icon matButtonIcon class="tb-mat-96">{{place.icon}}</tb-icon>
<span translate>{{place.name}}</span>
</a>
</mat-grid-tile>

2
ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.scss

@ -55,7 +55,7 @@
display: flex;
flex-direction: column;
align-items: center;
mat-icon {
.mat-icon {
margin: auto;
}
span.mdc-button__label {

6
ui-ngx/src/app/modules/home/components/widget/lib/rpc/persistent-table.component.html

@ -80,7 +80,7 @@
matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above"
(click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{ actionDescriptor.icon }}</mat-icon>
<tb-icon>{{ actionDescriptor.icon }}</tb-icon>
</button>
</ng-container>
</div>
@ -88,14 +88,14 @@
<button mat-icon-button
(click)="$event.stopPropagation(); ctx.detectChanges();"
[matMenuTriggerFor]="cellActionsMenu">
<mat-icon class="material-icons">more_vert</mat-icon>
<tb-icon>more_vert</tb-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<ng-container *ngFor="let actionDescriptor of actionCellButtonAction">
<button mat-menu-item *ngIf="actionDescriptor.icon"
[disabled]="(isLoading$ | async)"
(click)="onActionButtonClick($event, column, actionDescriptor)">
<mat-icon>{{actionDescriptor.icon}}</mat-icon>
<tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
<span>{{ actionDescriptor.displayName }}</span>
</button>
</ng-container>

105
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.html

@ -0,0 +1,105 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-color-settings-panel" [formGroup]="colorSettingsFormGroup">
<div class="tb-color-settings-title" translate>widgets.color.color-settings</div>
<div fxLayout="row">
<tb-toggle-select formControlName="type" fxFlex.xs fxFlex="70%">
<tb-toggle-option *ngFor="let type of colorTypes"
[value]="type">
{{ colorTypeTranslationsMap.get(type) | translate }}
</tb-toggle-option>
</tb-toggle-select>
</div>
<div class="tb-form-row space-between">
<div translate>widgets.color.color</div>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-color-settings-panel-body" [fxShow]="colorSettingsFormGroup.get('type').value === colorType.constant">
</div>
<div class="tb-color-settings-panel-body" [fxShow]="colorSettingsFormGroup.get('type').value === colorType.range">
<ng-container *ngTemplateOutlet="range"></ng-container>
</div>
<div class="tb-color-settings-panel-body" [fxShow]="colorSettingsFormGroup.get('type').value === colorType.function">
<ng-container *ngTemplateOutlet="function"></ng-container>
</div>
<div class="tb-color-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyColorSettings()"
[disabled]="colorSettingsFormGroup.invalid || !colorSettingsFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>
<ng-template #range>
<div fxFlex class="tb-color-ranges-panel">
<div class="tb-form-panel-title" translate>widgets.color.value-range</div>
<div class="tb-color-ranges" [formGroup]="colorSettingsFormGroup">
<div class="tb-form-row no-padding no-border" [formGroup]="rangeFormGroup" *ngFor="let rangeFormGroup of rangeListFormGroups; trackBy: trackByRange; let $index = index;">
<div fxFlex fxLayout="row" fxLayoutGap="24px">
<div fxFlex fxLayout="row" fxLayoutGap="12px" fxLayoutAlign="start center">
<div class="tb-value-range-text" translate>widgets.color.from</div>
<mat-form-field fxFlex appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput type="number" formControlName="from" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div class="tb-value-range-text tb-value-range-text-to" translate>widgets.color.to</div>
<mat-form-field fxFlex appearance="outline" class="center number" subscriptSizing="dynamic">
<input matInput type="number" formControlName="to" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<button type="button"
mat-icon-button
class="tb-box-button"
(click)="removeRange($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</div>
<button class="tb-add-color-range"
mat-stroked-button
(click)="addRange()">
<mat-icon>add</mat-icon>
</button>
</div>
</ng-template>
<ng-template #function>
<div class="tb-form-panel no-padding no-border" [formGroup]="colorSettingsFormGroup">
<tb-js-func formControlName="colorFunction"
[functionArgs]="['value']"
[globalVariables]="functionScopeVariables"
functionTitle="{{ 'widgets.color.color-function' | translate }}"
helpId="widget/lib/card/value_color_fn">
</tb-js-func>
</div>
</ng-template>

85
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.scss

@ -0,0 +1,85 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
.tb-color-settings-panel {
width: 500px;
height: 470px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-xs} {
width: 90vw;
}
.tb-color-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-color-ranges-panel {
flex: 1;
min-height: 0;
gap: 16px;
display: flex;
flex-direction: column;
}
.tb-color-ranges {
flex: 1;
gap: 12px;
display: flex;
flex-direction: column;
overflow: auto;
}
.tb-form-row {
height: auto;
.tb-value-range-text {
width: 64px;
font-size: 14px;
color: rgba(0, 0, 0, 0.38);
@media #{$mat-xs} {
width: auto;
}
&.tb-value-range-text-to {
text-align: center;
}
}
}
button.mat-mdc-button-base.tb-add-color-range {
&:not(:disabled) {
color: rgba(0, 0, 0, 0.54);
}
&:disabled {
color: rgba(0, 0, 0, 0.12);
}
}
.tb-color-settings-panel-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.tb-color-settings-panel-buttons {
height: 40px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

133
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings-panel.component.ts

@ -0,0 +1,133 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import {
ColorRange,
ColorSettings,
ColorType,
colorTypeTranslations
} from '@home/components/widget/config/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import {
AbstractControl,
FormControl,
FormGroup,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup
} from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Datasource, DatasourceType } from '@shared/models/widget.models';
import { deepClone } from '@core/utils';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { WidgetService } from '@core/http/widget.service';
@Component({
selector: 'tb-color-settings-panel',
templateUrl: './color-settings-panel.component.html',
providers: [],
styleUrls: ['./color-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ColorSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
colorSettings: ColorSettings;
@Input()
popover: TbPopoverComponent<ColorSettingsPanelComponent>;
@Output()
colorSettingsApplied = new EventEmitter<ColorSettings>();
colorType = ColorType;
colorTypes = Object.keys(ColorType) as ColorType[];
colorTypeTranslationsMap = colorTypeTranslations;
colorSettingsFormGroup: UntypedFormGroup;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.colorSettingsFormGroup = this.fb.group(
{
type: [this.colorSettings?.type, []],
color: [this.colorSettings?.color, []],
rangeList: this.fb.array((this.colorSettings?.rangeList || []).map(r => this.colorRangeControl(r))),
colorFunction: [this.colorSettings?.colorFunction, []]
}
);
this.colorSettingsFormGroup.get('type').valueChanges.subscribe(() => {
setTimeout(() => {this.popover?.updatePosition();}, 0);
});
}
private colorRangeControl(range: ColorRange): AbstractControl {
return this.fb.group({
from: [range?.from, []],
to: [range?.to, []],
color: [range?.color, []]
});
}
get rangeListFormArray(): UntypedFormArray {
return this.colorSettingsFormGroup.get('rangeList') as UntypedFormArray;
}
get rangeListFormGroups(): FormGroup[] {
return this.rangeListFormArray.controls as FormGroup[];
}
trackByRange(index: number, rangeControl: AbstractControl): any {
return rangeControl;
}
removeRange(index: number) {
this.rangeListFormArray.removeAt(index);
this.colorSettingsFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
addRange() {
const newRange: ColorRange = {
color: 'rgba(0,0,0,0.87)'
};
this.rangeListFormArray.push(this.colorRangeControl(newRange));
this.colorSettingsFormGroup.markAsDirty();
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
cancel() {
this.popover?.hide();
}
applyColorSettings() {
const colorSettings = this.colorSettingsFormGroup.value;
this.colorSettingsApplied.emit(colorSettings);
}
}

30
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.html

@ -0,0 +1,30 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<button type="button"
mat-stroked-button
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openColorSettingsPopup($event, matButton)">
<tb-icon matButtonIcon *ngIf="modelValue.type === colorType.function; else colorPreview">mdi:function-variant</tb-icon>
</button>
<ng-template #colorPreview>
<div class="tb-color-preview box" [ngClass]="{'disabled': disabled}">
<div class="tb-color-result" [style]="colorStyle"></div>
</div>
</ng-template>

124
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/color-settings.component.ts

@ -0,0 +1,124 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { ColorSettings, ColorType, ComponentStyle } from '@home/components/widget/config/widget-settings.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import {
ColorSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/color-settings-panel.component';
@Component({
selector: 'tb-color-settings',
templateUrl: './color-settings.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ColorSettingsComponent),
multi: true
}
]
})
export class ColorSettingsComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
colorType = ColorType;
modelValue: ColorSettings;
colorStyle: ComponentStyle = {};
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this.updateColorStyle();
}
writeValue(value: ColorSettings): void {
this.modelValue = value;
this.updateColorStyle();
}
openColorSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
colorSettings: this.modelValue
};
const colorSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorSettingsPanelComponent, 'left', true, null,
ctx,
{},
{}, {}, true);
colorSettingsPanelPopover.tbComponentRef.instance.popover = colorSettingsPanelPopover;
colorSettingsPanelPopover.tbComponentRef.instance.colorSettingsApplied.subscribe((colorSettings) => {
colorSettingsPanelPopover.hide();
this.modelValue = colorSettings;
this.updateColorStyle();
this.propagateChange(this.modelValue);
});
}
}
private updateColorStyle() {
if (!this.disabled) {
let colors: string[] = [this.modelValue.color];
if (this.modelValue.type === ColorType.range && this.modelValue.rangeList?.length) {
const rangeColors = this.modelValue.rangeList.slice(0, Math.min(2, this.modelValue.rangeList.length)).map(r => r.color);
colors = colors.concat(rangeColors);
}
if (colors.length === 1) {
this.colorStyle = {backgroundColor: colors[0]};
} else {
const gradientValues: string[] = [];
const step = 100 / colors.length;
for (let i = 0; i < colors.length; i++) {
gradientValues.push(`${colors[i]} ${step*i}%`);
gradientValues.push(`${colors[i]} ${step*(i+1)}%`);
}
this.colorStyle = {background: `linear-gradient(90deg, ${gradientValues.join(', ')})`};
}
} else {
this.colorStyle = {};
}
}
}

22
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.html

@ -0,0 +1,22 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 100%;">
<mat-select [formControl]="cssUnitFormControl">
<mat-option *ngFor="let cssUnit of cssUnitsList" [value]="cssUnit">{{ cssUnit }}</mat-option>
</mat-select>
</mat-form-field>

82
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/css-unit-select.component.ts

@ -0,0 +1,82 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { cssUnit, cssUnits } from '@home/components/widget/config/widget-settings.models';
@Component({
selector: 'tb-css-unit-select',
templateUrl: './css-unit-select.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CssUnitSelectComponent),
multi: true
}
]
})
export class CssUnitSelectComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
cssUnitsList = cssUnits;
cssUnitFormControl: UntypedFormControl;
modelValue: cssUnit;
private propagateChange = null;
constructor() {}
ngOnInit(): void {
this.cssUnitFormControl = new UntypedFormControl();
this.cssUnitFormControl.valueChanges.subscribe((value: cssUnit) => {
this.updateModel(value);
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.cssUnitFormControl.disable();
} else {
this.cssUnitFormControl.enable();
}
}
writeValue(value: cssUnit): void {
this.modelValue = value;
this.cssUnitFormControl.patchValue(this.modelValue, {emitEvent: false});
}
updateModel(value: cssUnit): void {
if (this.modelValue !== value) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
}

26
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.html

@ -0,0 +1,26 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-form-field appearance="outline" subscriptSizing="dynamic" style="width: 100%;">
<mat-select [formControl]="dateFormatFormControl" [compareWith]="dateFormatsCompare" >
<mat-option *ngFor="let dateFormat of dateFormatList" [value]="dateFormat">{{ dateFormatDisplayValue(dateFormat) }}</mat-option>
</mat-select>
<button #customFormatButton
*ngIf="dateFormatFormControl.value?.custom" matSuffix mat-icon-button (click)="openDateFormatSettingsPopup($event, customFormatButton)">
<tb-icon>edit</tb-icon>
</button>
</mat-form-field>

148
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-select.component.ts

@ -0,0 +1,148 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewChild, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import {
compareDateFormats,
dateFormats,
DateFormatSettings
} from '@home/components/widget/config/widget-settings.models';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { deepClone } from '@core/utils';
import {
DateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/date-format-settings-panel.component';
@Component({
selector: 'tb-date-format-select',
templateUrl: './date-format-select.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DateFormatSelectComponent),
multi: true
}
]
})
export class DateFormatSelectComponent implements OnInit, ControlValueAccessor {
@ViewChild('customFormatButton', {static: false})
customFormatButton: MatButton;
@Input()
disabled: boolean;
dateFormatList = dateFormats;
dateFormatsCompare = compareDateFormats;
dateFormatFormControl: UntypedFormControl;
modelValue: DateFormatSettings;
private propagateChange = null;
private formatCache: {[format: string]: string} = {};
constructor(private translate: TranslateService,
private date: DatePipe,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
this.dateFormatFormControl = new UntypedFormControl();
this.dateFormatFormControl.valueChanges.subscribe((value: DateFormatSettings) => {
this.updateModel(value);
if (value?.custom) {
setTimeout(() => {
this.openDateFormatSettingsPopup(null, this.customFormatButton);
}, 0);
}
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.dateFormatFormControl.disable();
} else {
this.dateFormatFormControl.enable();
}
}
writeValue(value: DateFormatSettings): void {
this.modelValue = value;
this.dateFormatFormControl.patchValue(this.modelValue, {emitEvent: false});
}
updateModel(value: DateFormatSettings): void {
if (!compareDateFormats(this.modelValue, value)) {
this.modelValue = value;
this.propagateChange(this.modelValue);
}
}
dateFormatDisplayValue(value: DateFormatSettings): string {
if (value.custom) {
return this.translate.instant('date.custom-date');
} else if (value.lastUpdateAgo) {
return this.translate.instant('date.last-update-n-ago');
} else {
if (!this.formatCache[value.format]) {
this.formatCache[value.format] = this.date.transform(Date.now(), value.format);
}
return this.formatCache[value.format];
}
}
openDateFormatSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
dateFormat: deepClone(this.modelValue)
};
const dateFormatSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, DateFormatSettingsPanelComponent, 'top', true, null,
ctx,
{},
{}, {}, true);
dateFormatSettingsPanelPopover.tbComponentRef.instance.popover = dateFormatSettingsPanelPopover;
dateFormatSettingsPanelPopover.tbComponentRef.instance.dateFormatApplied.subscribe((dateFormat) => {
dateFormatSettingsPanelPopover.hide();
this.modelValue = dateFormat;
this.propagateChange(this.modelValue);
});
}
}
}

47
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.html

@ -0,0 +1,47 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-date-format-settings-panel">
<div class="tb-date-format-settings-title" translate>date.custom-date</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>date.format</div>
<mat-form-field class="tb-date-format-input" required fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput [formControl]="dateFormatFormControl" placeholder="{{ 'widget-config.set' | translate }}">
<div matSuffix tb-help-popup="date/date-format" [tb-help-popup-style]="{width: '800px'}"></div>
</mat-form-field>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding date-format-preview">
<div class="fixed-title-width" translate>date.preview</div>
<div class="preview-text" fxFlex>{{ previewText }}</div>
</div>
<div class="tb-date-format-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyDateFormat()"
[disabled]="dateFormatFormControl.invalid || !dateFormatFormControl.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

67
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.scss

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../../../../scss/constants';
.tb-date-format-settings-panel {
width: 500px;
display: flex;
flex-direction: column;
gap: 16px;
@media #{$mat-xs} {
width: 90vw;
}
.tb-date-format-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
}
&.date-format-preview {
align-items: flex-start;
.preview-text {
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.38);
}
}
.mat-mdc-form-field.tb-date-format-input {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
.mat-mdc-form-field-icon-suffix {
display: flex;
align-items: center;
line-height: normal;
}
}
}
}
.tb-date-format-settings-panel-buttons {
height: 60px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

70
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/date-format-settings-panel.component.ts

@ -0,0 +1,70 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { DateFormatSettings } from '@home/components/widget/config/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormControl, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { DatePipe } from '@angular/common';
@Component({
selector: 'tb-date-format-settings-panel',
templateUrl: './date-format-settings-panel.component.html',
providers: [],
styleUrls: ['./date-format-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class DateFormatSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
dateFormat: DateFormatSettings;
@Input()
popover: TbPopoverComponent<DateFormatSettingsPanelComponent>;
@Output()
dateFormatApplied = new EventEmitter<DateFormatSettings>();
dateFormatFormControl: UntypedFormControl;
previewText = '';
constructor(private date: DatePipe,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.dateFormatFormControl = new UntypedFormControl(this.dateFormat.format, [Validators.required]);
this.dateFormatFormControl.valueChanges.subscribe((value: string) => {
this.previewText = this.date.transform(Date.now(), value);
});
this.previewText = this.date.transform(Date.now(), this.dateFormat.format);
}
cancel() {
this.popover?.hide();
}
applyDateFormat() {
this.dateFormat.format = this.dateFormatFormControl.value;
this.dateFormatApplied.emit(this.dateFormat);
}
}

91
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html

@ -0,0 +1,91 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-font-settings-panel" [formGroup]="fontFormGroup">
<div class="tb-font-settings-title" translate>widgets.widget-font.font-settings</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.size</div>
<div fxFlex fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="12px">
<mat-form-field fxFlex appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput type="number" min="0" formControlName="size" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<tb-css-unit-select fxFlex formControlName="sizeUnit"></tb-css-unit-select>
</div>
</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.font-family</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput #familyInput
formControlName="family" placeholder="{{ 'widget-config.set' | translate }}"
[matAutocomplete]="familyAutocomplete">
<button *ngIf="fontFormGroup.get('family').value"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clearFamily()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
#familyAutocomplete="matAutocomplete"
class="tb-autocomplete"
panelWidth="fit-content">
<mat-option *ngFor="let family of filteredFontFamilies | async" [value]="family">
<span [innerHTML]="family | highlight:familySearchText:true:'ig'"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.font-weight</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="weight">
<mat-option *ngFor="let weight of fontWeightsList" [value]="weight">
{{ fontWeightTranslationsMap.has(weight) ? (fontWeightTranslationsMap.get(weight) | translate) : weight }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row no-border no-padding">
<div class="fixed-title-width" translate>widgets.widget-font.font-style</div>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="style">
<mat-option *ngFor="let style of fontStylesList" [value]="style">
{{ fontStyleTranslationsMap.get(style) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-divider></mat-divider>
<div class="tb-form-row no-border no-padding font-preview">
<div class="fixed-title-width" translate>widgets.widget-font.preview</div>
<div class="preview-text" fxFlex [style]="previewStyle">{{ previewText }}</div>
</div>
<div class="tb-font-settings-panel-buttons">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button
color="primary"
type="button"
(click)="applyFont()"
[disabled]="fontFormGroup.invalid || !fontFormGroup.dirty">
{{ 'action.apply' | translate }}
</button>
</div>
</div>

51
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss

@ -0,0 +1,51 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-font-settings-panel {
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
.tb-font-settings-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-form-row {
.fixed-title-width {
min-width: 120px;
}
&.font-preview {
align-items: flex-start;
.preview-text {
max-height: 300px;
max-width: 400px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.tb-font-settings-panel-buttons {
height: 60px;
display: flex;
flex-direction: row;
gap: 16px;
justify-content: flex-end;
align-items: flex-end;
}
}

133
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts

@ -0,0 +1,133 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
Component,
ElementRef,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import {
commonFonts,
ComponentStyle,
Font,
fontStyles,
fontStyleTranslations,
fontWeights,
fontWeightTranslations,
textStyle
} from '@home/components/widget/config/widget-settings.models';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Observable } from 'rxjs';
import { map, startWith, tap } from 'rxjs/operators';
@Component({
selector: 'tb-font-settings-panel',
templateUrl: './font-settings-panel.component.html',
providers: [],
styleUrls: ['./font-settings-panel.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class FontSettingsPanelComponent extends PageComponent implements OnInit {
@Input()
font: Font;
@Input()
previewText = 'AaBbCcDd';
@Input()
popover: TbPopoverComponent<FontSettingsPanelComponent>;
@Output()
fontApplied = new EventEmitter<Font>();
@ViewChild('familyInput', {static: true}) familyInput: ElementRef;
fontWeightsList = fontWeights;
fontWeightTranslationsMap = fontWeightTranslations;
fontStylesList = fontStyles;
fontStyleTranslationsMap = fontStyleTranslations;
fontFormGroup: UntypedFormGroup;
filteredFontFamilies: Observable<Array<string>>;
familySearchText = '';
previewStyle: ComponentStyle = {};
constructor(private fb: UntypedFormBuilder,
protected store: Store<AppState>) {
super(store);
}
ngOnInit(): void {
this.fontFormGroup = this.fb.group(
{
size: [this.font?.size, [Validators.required, Validators.min(0)]],
sizeUnit: [this.font?.sizeUnit, [Validators.required]],
family: [this.font?.family, [Validators.required]],
weight: [this.font?.weight, [Validators.required]],
style: [this.font?.style, [Validators.required]]
}
);
if (this.font) {
this.previewStyle = textStyle(this.font, '1');
}
this.fontFormGroup.valueChanges.subscribe((value: Font) => {
if (this.fontFormGroup.valid) {
this.previewStyle = textStyle(value, '1');
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
});
this.filteredFontFamilies = this.fontFormGroup.get('family').valueChanges
.pipe(
startWith<string>(''),
tap((searchText) => { this.familySearchText = searchText || ''; }),
map(() => commonFonts.filter(f => f.toUpperCase().includes(this.familySearchText.toUpperCase())))
);
}
clearFamily() {
this.fontFormGroup.get('family').patchValue(null, {emitEvent: true});
setTimeout(() => {
this.familyInput.nativeElement.blur();
this.familyInput.nativeElement.focus();
}, 0);
}
cancel() {
this.popover?.hide();
}
applyFont() {
const font = this.fontFormGroup.value;
this.fontApplied.emit(font);
}
}

25
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.html

@ -0,0 +1,25 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<button type="button"
mat-stroked-button
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openFontSettingsPopup($event, matButton)">
<tb-icon matButtonIcon>text_format</tb-icon>
</button>

102
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts

@ -0,0 +1,102 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Font } from '@home/components/widget/config/widget-settings.models';
import { MatButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service';
import { FontSettingsPanelComponent } from '@home/components/widget/lib/settings/common/font-settings-panel.component';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-font-settings',
templateUrl: './font-settings.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FontSettingsComponent),
multi: true
}
]
})
export class FontSettingsComponent implements OnInit, ControlValueAccessor {
@Input()
disabled: boolean;
@Input()
previewText: string | (() => string);
private modelValue: Font;
private propagateChange = null;
constructor(private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: Font): void {
this.modelValue = value;
}
openFontSettingsPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const ctx: any = {
font: this.modelValue
};
if (isDefinedAndNotNull(this.previewText)) {
const previewText = typeof this.previewText === 'string' ? this.previewText : this.previewText();
if (previewText) {
ctx.previewText = previewText;
}
}
const fontSettingsPanelPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, FontSettingsPanelComponent, 'left', true, null,
ctx,
{},
{}, {}, true);
fontSettingsPanelPopover.tbComponentRef.instance.popover = fontSettingsPanelPopover;
fontSettingsPanelPopover.tbComponentRef.instance.fontApplied.subscribe((font) => {
fontSettingsPanelPopover.hide();
this.modelValue = font;
this.propagateChange(this.modelValue);
});
}
}
}

43
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html

@ -0,0 +1,43 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-image-cards-select tb-form-panel stroked no-padding">
<mat-expansion-panel class="tb-settings" [expanded]="expanded" disabled>
<mat-expansion-panel-header class="fill-width" fxLayout="row wrap">
<div fxFlex class="tb-form-row no-border" [class.expanded]="expanded">
<div class="fixed-title-width">{{ label }}</div>
<mat-form-field class="tb-image-cards-value-field" fxFlex appearance="outline" subscriptSizing="dynamic" (click)="toggleSelectPanel($event)">
<input readonly matInput [formControl]="valueFormControl" placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix>{{ expanded ? 'expand_less' : 'expand_more' }}</mat-icon>
</mat-form-field>
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<mat-grid-list class="tb-image-cards-options" [cols]="cols$ | async" [rowHeight]="rowHeight" gutterSize="8">
<mat-grid-tile *ngFor="let option of options" (click)="updateModel(option.value)">
<div class="tb-image-cards-option" [class.selected]="modelValue === option.value">
<div class="tb-image-cards-option-background"></div>
<div class="tb-image-cards-option-title">{{ option.name }}</div>
<div class="tb-image-cards-option-image-container">
<img class="tb-image-cards-option-image" src="{{ option.image }}"/>
</div>
</div>
</mat-grid-tile>
</mat-grid-list>
</ng-template>
</mat-expansion-panel>
</div>

116
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.scss

@ -0,0 +1,116 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-image-cards-select.tb-form-panel {
.tb-form-row {
transition: all .3s;
&.expanded {
padding: 11px 7px 11px 16px;
}
}
.tb-image-cards-value-field {
cursor: pointer;
user-select: none;
input {
cursor: pointer;
pointer-events: none;
}
}
.mat-expansion-panel {
&.tb-settings {
> .mat-expansion-panel-header {
height: auto;
.mat-content {
margin: 0;
}
.tb-form-row {
font-weight: normal;
font-size: 16px;
color: rgba(0, 0, 0, 0.87);
}
.mat-expansion-indicator {
display: none;
}
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0 16px 16px !important;
}
}
}
}
.tb-image-cards-option {
width: 100%;
height: 100%;
cursor: pointer;
padding: 8px 12px 12px 12px;
display: flex;
flex-direction: column;
gap: 8px;
align-items: start;
position: relative;
.tb-image-cards-option-background {
border-radius: 4px;
position: absolute;
top: 1px;
left: 1px;
right: 1px;
bottom: 1px;
background: rgba(0, 0, 0, 0.04);
}
&:before {
content: unset;
border-radius: 4px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.tb-image-cards-option-title {
z-index: 1;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
}
.tb-image-cards-option-image-container {
z-index: 1;
flex: 1;
width: 100%;
min-height: 0;
display: flex;
justify-content: center;
}
&.selected {
.tb-image-cards-option-background {
background: #305680;
opacity: 0.04;
}
&:before {
content: "";
border: 1px solid #305680;
opacity: 0.32;
}
.tb-image-cards-option-title {
font-size: 13px;
font-weight: 500;
color: #305680;
}
}
}
}

190
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts

@ -0,0 +1,190 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
AfterContentInit,
Component,
ContentChildren,
Directive,
ElementRef,
forwardRef,
Input,
OnDestroy, OnInit,
QueryList,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormControl } from '@angular/forms';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Observable, Subject } from 'rxjs';
import { map, share, startWith, takeUntil } from 'rxjs/operators';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
export interface ImageCardsSelectOption {
name: string;
value: any;
image: string;
}
@Directive(
{
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'tb-image-cards-select-option',
}
)
export class ImageCardsSelectOptionDirective {
@Input() value: any;
@Input() image: string;
get viewValue(): string {
return (this._element?.nativeElement.textContent || '').trim();
}
constructor(
private _element: ElementRef<HTMLElement>
) {}
}
@Component({
selector: 'tb-image-cards-select',
templateUrl: './image-cards-select.component.html',
styleUrls: ['./image-cards-select.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ImageCardsSelectComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class ImageCardsSelectComponent implements ControlValueAccessor, OnInit, AfterContentInit, OnDestroy {
@ContentChildren(ImageCardsSelectOptionDirective) imageCardsSelectOptions: QueryList<ImageCardsSelectOptionDirective>;
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
cols = 4;
@Input()
colsLtMd = 2;
@Input()
rowHeight = '9:5';
@Input()
label: string;
valueFormControl: UntypedFormControl;
options: ImageCardsSelectOption[] = [];
modelValue: any;
expanded = false;
cols$: Observable<number>;
private propagateChange = null;
private _destroyed = new Subject<void>();
constructor(private breakpointObserver: BreakpointObserver) {
this.valueFormControl = new UntypedFormControl('');
}
ngOnInit(): void {
const gridColumns = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']) ? this.colsLtMd : this.cols;
this.cols$ = this.breakpointObserver
.observe(MediaBreakpoints['lt-md']).pipe(
map((state) => state.matches ? this.colsLtMd : this.cols),
startWith(gridColumns),
share()
);
}
ngAfterContentInit(): void {
this.imageCardsSelectOptions.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
this.syncImageCardsSelectOptions();
});
}
ngOnDestroy() {
this._destroyed.next();
this._destroyed.complete();
}
private syncImageCardsSelectOptions() {
if (this.imageCardsSelectOptions?.length) {
this.options.length = 0;
this.imageCardsSelectOptions.forEach(option => {
this.options.push(
{ name: option.viewValue,
value: option.value,
image: option.image
}
);
});
this.updateDisplayValue();
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.valueFormControl.disable();
} else {
this.valueFormControl.enable();
}
}
writeValue(value: any): void {
this.modelValue = value;
this.updateDisplayValue();
}
updateModel(value: any) {
this.modelValue = value;
this.updateDisplayValue();
this.propagateChange(this.modelValue);
this.expanded = false;
}
toggleSelectPanel($event: Event) {
$event.stopPropagation();
if (!this.disabled) {
this.expanded = !this.expanded;
}
}
private updateDisplayValue() {
const currentOption = this.options.find(o => o.value === this.modelValue);
const displayValue = currentOption ? currentOption.name : '';
this.valueFormControl.patchValue(displayValue, {emitEvent: false});
}
}

37
ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts

@ -266,6 +266,21 @@ import {
QuickLinksWidgetSettingsComponent
} from '@home/components/widget/lib/settings/home-page/quick-links-widget-settings.component';
import { LegendConfigComponent } from '@home/components/widget/lib/settings/common/legend-config.component';
import {
ImageCardsSelectOptionDirective,
ImageCardsSelectComponent
} from '@home/components/widget/lib/settings/common/image-cards-select.component';
import { FontSettingsComponent } from '@home/components/widget/lib/settings/common/font-settings.component';
import { FontSettingsPanelComponent } from '@home/components/widget/lib/settings/common/font-settings-panel.component';
import { ColorSettingsComponent } from '@home/components/widget/lib/settings/common/color-settings.component';
import {
ColorSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/color-settings-panel.component';
import { CssUnitSelectComponent } from '@home/components/widget/lib/settings/common/css-unit-select.component';
import { DateFormatSelectComponent } from '@home/components/widget/lib/settings/common/date-format-select.component';
import {
DateFormatSettingsPanelComponent
} from '@home/components/widget/lib/settings/common/date-format-settings-panel.component';
@NgModule({
declarations: [
@ -367,7 +382,16 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent
QuickLinksWidgetSettingsComponent,
ImageCardsSelectOptionDirective,
ImageCardsSelectComponent,
FontSettingsComponent,
FontSettingsPanelComponent,
ColorSettingsComponent,
ColorSettingsPanelComponent,
CssUnitSelectComponent,
DateFormatSelectComponent,
DateFormatSettingsPanelComponent
],
imports: [
CommonModule,
@ -473,7 +497,16 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent
QuickLinksWidgetSettingsComponent,
ImageCardsSelectOptionDirective,
ImageCardsSelectComponent,
FontSettingsComponent,
FontSettingsPanelComponent,
ColorSettingsComponent,
ColorSettingsPanelComponent,
CssUnitSelectComponent,
DateFormatSelectComponent,
DateFormatSettingsPanelComponent
]
})
export class WidgetSettingsModule {

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

@ -540,6 +540,12 @@ export class WidgetComponentService {
if (isUndefined(result.typeParameters.processNoDataByWidget)) {
result.typeParameters.processNoDataByWidget = false;
}
if (isUndefined(result.typeParameters.previewWidth)) {
result.typeParameters.previewWidth = '100%';
}
if (isUndefined(result.typeParameters.previewHeight)) {
result.typeParameters.previewHeight = '70%';
}
if (isFunction(widgetTypeInstance.actionSources)) {
result.actionSources = widgetTypeInstance.actionSources();
} else {

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

@ -43,6 +43,7 @@ import { HomePageWidgetsModule } from '@home/components/widget/lib/home-page/hom
import { WIDGET_COMPONENTS_MODULE_TOKEN } from '@home/components/tokens';
import { FlotWidgetComponent } from '@home/components/widget/lib/flot-widget.component';
import { LegendComponent } from '@home/components/widget/lib/legend.component';
import { ValueCardWidgetComponent } from '@home/components/widget/lib/cards/value-card-widget.component';
@NgModule({
declarations:
@ -66,7 +67,8 @@ import { LegendComponent } from '@home/components/widget/lib/legend.component';
MarkdownWidgetComponent,
SelectEntityDialogComponent,
LegendComponent,
FlotWidgetComponent
FlotWidgetComponent,
ValueCardWidgetComponent
],
imports: [
CommonModule,
@ -94,7 +96,8 @@ import { LegendComponent } from '@home/components/widget/lib/legend.component';
QrCodeWidgetComponent,
MarkdownWidgetComponent,
LegendComponent,
FlotWidgetComponent
FlotWidgetComponent,
ValueCardWidgetComponent
],
providers: [
{provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule }

14
ui-ngx/src/app/modules/home/components/widget/widget-container.component.html

@ -41,7 +41,7 @@
matTooltipClass="tb-tooltip-multiline"
matTooltipPosition="above"
class="mat-subtitle-1 title">
<mat-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</mat-icon>
<tb-icon *ngIf="widget.showTitleIcon" [ngStyle]="widget.titleIconStyle">{{widget.titleIcon}}</tb-icon>
{{widget.customTranslatedTitle}}
</span>
<tb-timewindow *ngIf="widget.hasTimewindow"
@ -66,42 +66,42 @@
(click)="action.onAction($event)"
matTooltip="{{ action.displayName }}"
matTooltipPosition="above">
<mat-icon>{{ action.icon }}</mat-icon>
<tb-icon>{{ action.icon }}</tb-icon>
</button>
<button mat-icon-button *ngFor="let action of widget.widgetActions"
[fxShow]="!isEdit && action.show"
(click)="action.onAction($event)"
matTooltip="{{ action.name | translate }}"
matTooltipPosition="above">
<mat-icon>{{ action.icon }}</mat-icon>
<tb-icon>{{ action.icon }}</tb-icon>
</button>
<button mat-icon-button
[fxShow]="!isEdit && widget.enableFullscreen"
(click)="$event.stopPropagation(); widget.isFullscreen = !widget.isFullscreen"
matTooltip="{{(widget.isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above">
<mat-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
<tb-icon>{{ widget.isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</tb-icon>
</button>
<button mat-icon-button
[fxShow]="isEditActionEnabled && !widget.isFullscreen"
(click)="onEdit($event)"
matTooltip="{{ 'widget.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
[fxShow]="isExportActionEnabled && !widget.isFullscreen"
(click)="onExport($event)"
matTooltip="{{ 'widget.export' | translate }}"
matTooltipPosition="above">
<mat-icon>file_download</mat-icon>
<tb-icon>file_download</tb-icon>
</button>
<button mat-icon-button
[fxShow]="isRemoveActionEnabled && !widget.isFullscreen"
(click)="onRemove($event);"
matTooltip="{{ 'widget.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
<tb-icon>close</tb-icon>
</button>
</div>
</div>

2
ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss

@ -82,7 +82,7 @@ div.tb-widget {
margin: 0 !important;
line-height: 20px;
mat-icon {
.mat-icon {
width: 20px;
min-width: 20px;
height: 20px;

7
ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts

@ -15,6 +15,7 @@
///
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@ -62,7 +63,7 @@ export class WidgetComponentAction {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class WidgetContainerComponent extends PageComponent implements OnInit, OnDestroy {
export class WidgetContainerComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
@HostBinding('class')
widgetContainerClass = 'tb-widget-container';
@ -131,6 +132,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}
}
ngAfterViewInit(): void {
this.widget.widgetContext.$widgetElement = $(this.tbWidgetElement.nativeElement);
}
ngOnDestroy(): void {
if (this.cssClass) {
const el = this.document.getElementById(this.cssClass);

2
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html

@ -16,6 +16,8 @@
-->
<tb-dashboard class="tb-preview-dashboard"
[style.width]="previewWidth"
[style.height]="previewHeight"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboardTimewindow"

9
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss

@ -16,12 +16,11 @@
:host {
z-index: 10;
background: #F3F6FA;
display: flex;
align-items: center;
justify-content: center;
.tb-preview-dashboard {
position: absolute;
top: 15%;
bottom: 15%;
left: 0;
right: 0;
position: relative;
}
.tb-preview-panel {
position: absolute;

6
ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts

@ -45,6 +45,12 @@ export class WidgetPreviewComponent extends PageComponent implements OnInit, OnC
@Input()
widgetConfig: WidgetConfig;
@Input()
previewWidth = '100%';
@Input()
previewHeight = '70%';
widgets: Widget[];
constructor(protected store: Store<AppState>) {

3
ui-ngx/src/app/modules/home/menu/menu-link.component.html

@ -16,7 +16,6 @@
-->
<a mat-button routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'subset', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}">
<mat-icon *ngIf="!section.isMdiIcon && section.icon !== null">{{section.icon}}</mat-icon>
<mat-icon *ngIf="section.isMdiIcon && section.icon !== null" [svgIcon]="section.icon"></mat-icon>
<tb-icon matButtonIcon *ngIf="section.icon !== null">{{section.icon}}</tb-icon>
<span>{{section.name | translate}}</span>
</a>

3
ui-ngx/src/app/modules/home/menu/menu-toggle.component.html

@ -16,8 +16,7 @@
-->
<a mat-button class="tb-button-toggle" (click)="toggleSection($event)">
<mat-icon *ngIf="!section.isMdiIcon && section.icon !== null">{{section.icon}}</mat-icon>
<mat-icon *ngIf="section.isMdiIcon && section.icon !== null" [svgIcon]="section.icon"></mat-icon>
<tb-icon matButtonIcon *ngIf="section.icon !== null">{{section.icon}}</tb-icon>
<span>{{section.name | translate}}</span>
<span class=" pull-right fa fa-chevron-down tb-toggle-icon"
[ngClass]="{'tb-toggled' : section.opened}"></span>

3
ui-ngx/src/app/modules/home/models/dashboard-component.models.ts

@ -446,7 +446,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
this.titleIconStyle.color = this.widget.config.iconColor;
}
if (this.widget.config.iconSize) {
this.titleIconStyle.width = this.widget.config.iconSize;
this.titleIconStyle.height = this.widget.config.iconSize;
this.titleIconStyle.fontSize = this.widget.config.iconSize;
this.titleIconStyle.lineHeight = this.widget.config.iconSize;
}
this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true;

1
ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts

@ -75,7 +75,6 @@ export interface GroupActionDescriptor<T extends BaseData<HasId>> {
export interface HeaderActionDescriptor {
name: string;
icon: string;
isMdiIcon?: boolean;
isEnabled: () => boolean;
onAction: ($event: MouseEvent) => void;
}

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

@ -243,6 +243,7 @@ export class WidgetContext {
formatValue
};
$widgetElement: JQuery<HTMLElement>;
$container: JQuery<HTMLElement>;
$containerParent: JQuery<HTMLElement>;
width: number;

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

@ -319,8 +319,7 @@ const routes: Routes = [
title: 'admin.2fa.2fa',
breadcrumb: {
label: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication',
isMdiIcon: true
icon: 'mdi:two-factor-authentication'
}
}
},

3
ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html

@ -27,8 +27,7 @@
<mat-grid-list rowHeight="170px" [cols]="section.places.length">
<mat-grid-tile *ngFor="let place of section.places">
<a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">
<mat-icon *ngIf="!place.isMdiIcon" class="material-icons tb-mat-96">{{place.icon}}</mat-icon>
<mat-icon *ngIf="place.isMdiIcon" class="tb-mat-96" [svgIcon]="place.icon"></mat-icon>
<tb-icon matButtonIcon class="tb-mat-96">{{place.icon}}</tb-icon>
<span translate>{{place.name}}</span>
</a>
</mat-grid-tile>

2
ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss

@ -55,7 +55,7 @@
display: flex;
flex-direction: column;
align-items: center;
mat-icon {
.mat-icon {
margin: auto;
}
span.mdc-button__label {

8
ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html

@ -34,13 +34,9 @@
<button *ngFor="let type of allWidgetTypes;" class="tb-card-button" mat-raised-button color="primary"
type="button"
(click)="typeSelected(widgetTypes[type])">
<mat-icon *ngIf="!widgetTypesDataMap.get(widgetTypes[type]).isMdiIcon; else mdiIconBlock" class="tb-mat-96">
<tb-icon matButtonIcon class="tb-mat-96">
{{ widgetTypesDataMap.get(widgetTypes[type]).icon }}
</mat-icon>
<ng-template #mdiIconBlock>
<mat-icon class="tb-mat-96" [svgIcon]="widgetTypesDataMap.get(widgetTypes[type]).icon">
</mat-icon>
</ng-template>
</tb-icon>
<span translate>{{ widgetTypesDataMap.get(widgetTypes[type]).name }}</span>
</button>
</div>

2
ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss

@ -21,7 +21,7 @@
display: flex;
flex-direction: column;
align-items: center;
mat-icon {
.mat-icon {
margin: auto;
}
span.mdc-button__label {

6
ui-ngx/src/app/shared/components/breadcrumb.component.html

@ -37,11 +37,9 @@
</div>
<ng-template #breadcrumbWithIcon let-breadcrumb="breadcrumb">
<img *ngIf="breadcrumb.iconUrl" [src]="breadcrumb.iconUrl"/>
<mat-icon *ngIf="!breadcrumb.iconUrl && breadcrumb.isMdiIcon" [svgIcon]="breadcrumb.icon">
</mat-icon>
<mat-icon *ngIf="!breadcrumb.iconUrl && !breadcrumb.isMdiIcon">
<tb-icon *ngIf="!breadcrumb.iconUrl">
{{ breadcrumb.icon }}
</mat-icon>
</tb-icon>
{{ breadcrumb.ignoreTranslate
? (breadcrumb.labelFunction ? breadcrumb.labelFunction() : utils.customTranslation(breadcrumb.label, breadcrumb.label))
: (breadcrumb.label | translate) }}

2
ui-ngx/src/app/shared/components/breadcrumb.component.ts

@ -117,7 +117,6 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
ignoreTranslate = false;
}
const icon = breadcrumbConfig.icon || 'home';
const isMdiIcon = icon.startsWith('mdi:');
const link = [ route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/') ];
const breadcrumb = {
id: guid(),
@ -125,7 +124,6 @@ export class BreadcrumbComponent implements OnInit, OnDestroy {
labelFunction,
ignoreTranslate,
icon,
isMdiIcon,
link,
queryParams: null
};

1
ui-ngx/src/app/shared/components/breadcrumb.ts

@ -23,7 +23,6 @@ export interface BreadCrumb extends HasUUID{
labelFunction?: () => string;
ignoreTranslate: boolean;
icon: string;
isMdiIcon: boolean;
link: any[];
queryParams: Params;
}

4
ui-ngx/src/app/shared/components/color-input.component.html

@ -37,12 +37,12 @@
<ng-template #boxInput>
<button type="button"
mat-stroked-button
class="color-box"
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openColorPickerPopup($event, matButton)">
<div class="tb-color-preview no-margin box" [ngClass]="{'disabled': disabled}">
<div class="tb-color-result" [ngStyle]="!disabled ? {background: colorFormGroup.get('color').value} : {}"></div>
<div class="tb-color-result" [style]="!disabled ? {background: colorFormGroup.get('color').value} : {}"></div>
</div>
</button>
</ng-template>

6
ui-ngx/src/app/shared/components/color-input.component.scss

@ -32,10 +32,4 @@
margin: 0;
}
}
button.mat-mdc-button-base.color-box {
width: 40px;
min-width: 40px;
height: 40px;
padding: 7px;
}
}

5
ui-ngx/src/app/shared/components/help-markdown.component.ts

@ -24,6 +24,7 @@ import {
import { BehaviorSubject } from 'rxjs';
import { share } from 'rxjs/operators';
import { HelpService } from '@core/services/help.service';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-help-markdown',
@ -36,7 +37,9 @@ export class HelpMarkdownComponent implements OnDestroy, OnInit, OnChanges {
@Input() helpContent: string;
@Input() visible: boolean;
@Input()
@coerceBoolean()
visible: boolean;
@Input() style: { [klass: string]: any } = {};

281
ui-ngx/src/app/shared/components/icon.component.ts

@ -0,0 +1,281 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { CanColor, mixinColor } from '@angular/material/core';
import {
AfterContentInit,
AfterViewChecked,
ChangeDetectionStrategy,
Component,
ElementRef,
ErrorHandler,
Inject,
OnDestroy,
Renderer2,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { MAT_ICON_LOCATION, MatIconLocation, MatIconRegistry } from '@angular/material/icon';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { isSvgIcon, splitIconName } from '@shared/models/icon.models';
import { ContentObserver } from '@angular/cdk/observers';
const _TbIconBase = mixinColor(
class {
constructor(public _elementRef: ElementRef) {}
},
);
const funcIriAttributes = [
'clip-path',
'color-profile',
'src',
'cursor',
'fill',
'filter',
'marker',
'marker-start',
'marker-mid',
'marker-end',
'mask',
'stroke',
];
const funcIriAttributeSelector = funcIriAttributes.map(attr => `[${attr}]`).join(', ');
const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/;
@Component({
template: '<span style="display: none;" #iconNameContent><ng-content></ng-content></span>',
selector: 'tb-icon',
exportAs: 'tbIcon',
styleUrls: [],
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['color'],
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: {
role: 'img',
class: 'mat-icon notranslate',
'[attr.data-mat-icon-type]': '!_useSvgIcon ? "font" : "svg"',
'[attr.data-mat-icon-name]': '_svgName',
'[attr.data-mat-icon-namespace]': '_svgNamespace',
'[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TbIconComponent extends _TbIconBase
implements AfterContentInit, AfterViewChecked, CanColor, OnDestroy {
@ViewChild('iconNameContent', {static: true})
_iconNameContent: ElementRef;
private icon: string;
get viewValue(): string {
return (this._iconNameContent?.nativeElement.textContent || '').trim();
}
private _contentChanges: Subscription = null;
private _previousFontSetClass: string[] = [];
_useSvgIcon = false;
_svgName: string | null;
_svgNamespace: string | null;
private _textElement = null;
private _previousPath?: string;
private _elementsWithExternalReferences?: Map<Element, {name: string; value: string}[]>;
private _currentIconFetch = Subscription.EMPTY;
constructor(elementRef: ElementRef<HTMLElement>,
private contentObserver: ContentObserver,
private renderer: Renderer2,
private _iconRegistry: MatIconRegistry,
@Inject(MAT_ICON_LOCATION) private _location: MatIconLocation,
private readonly _errorHandler: ErrorHandler) {
super(elementRef);
}
ngAfterContentInit(): void {
this.icon = this.viewValue;
this._updateIcon();
this._contentChanges = this.contentObserver.observe(this._iconNameContent.nativeElement)
.subscribe(() => {
const content = this.viewValue;
if (content && this.icon !== content) {
this.icon = content;
this._updateIcon();
}
});
}
ngAfterViewChecked() {
const cachedElements = this._elementsWithExternalReferences;
if (cachedElements && cachedElements.size) {
const newPath = this._location.getPathname();
if (newPath !== this._previousPath) {
this._previousPath = newPath;
this._prependPathToReferences(newPath);
}
}
}
ngOnDestroy() {
this._contentChanges.unsubscribe();
this._currentIconFetch.unsubscribe();
if (this._elementsWithExternalReferences) {
this._elementsWithExternalReferences.clear();
}
}
private _updateIcon() {
const useSvgIcon = isSvgIcon(this.icon);
if (this._useSvgIcon !== useSvgIcon) {
this._useSvgIcon = useSvgIcon;
if (!this._useSvgIcon) {
this._updateSvgIcon(undefined);
} else {
this._updateFontIcon(undefined);
}
}
if (this._useSvgIcon) {
this._updateSvgIcon(this.icon);
} else {
this._updateFontIcon(this.icon);
}
}
private _updateFontIcon(rawName: string | undefined) {
if (rawName) {
this._clearFontIcon();
const iconName = splitIconName(rawName)[1];
this._textElement = this.renderer.createText(iconName);
const elem: HTMLElement = this._elementRef.nativeElement;
this.renderer.insertBefore(elem, this._textElement, this._iconNameContent.nativeElement);
const fontSetClasses = (
this._iconRegistry.getDefaultFontSetClass()
).filter(className => className.length > 0);
fontSetClasses.forEach(className => elem.classList.add(className));
this._previousFontSetClass = fontSetClasses;
} else {
this._clearFontIcon();
}
}
private _clearFontIcon() {
const elem: HTMLElement = this._elementRef.nativeElement;
if (this._textElement !== null) {
this.renderer.removeChild(elem, this._textElement);
this._textElement = null;
}
this._previousFontSetClass.forEach(className => elem.classList.remove(className));
this._previousFontSetClass = [];
}
private _updateSvgIcon(rawName: string | undefined) {
this._svgNamespace = null;
this._svgName = null;
this._currentIconFetch.unsubscribe();
if (rawName) {
const [namespace, iconName] = splitIconName(rawName);
if (namespace) {
this._svgNamespace = namespace;
}
if (iconName) {
this._svgName = iconName;
}
this._iconRegistry.getDefaultFontSetClass();
this._currentIconFetch = this._iconRegistry
.getNamedSvgIcon(iconName, namespace)
.pipe(take(1))
.subscribe({
next: (svg) => this._setSvgElement(svg),
error: (err: Error) => {
const errorMessage = `Error retrieving icon ${namespace}:${iconName}! ${err.message}`;
this._errorHandler.handleError(new Error(errorMessage));
}
});
} else {
this._clearSvgElement();
}
}
private _setSvgElement(svg: SVGElement) {
this._clearSvgElement();
const path = this._location.getPathname();
this._previousPath = path;
this._cacheChildrenWithExternalReferences(svg);
this._prependPathToReferences(path);
this.renderer.insertBefore(this._elementRef.nativeElement, svg, this._iconNameContent.nativeElement);
}
private _clearSvgElement() {
const layoutElement: HTMLElement = this._elementRef.nativeElement;
let childCount = layoutElement.childNodes.length;
if (this._elementsWithExternalReferences) {
this._elementsWithExternalReferences.clear();
}
while (childCount--) {
const child = layoutElement.childNodes[childCount];
if (child.nodeType !== 1 || child.nodeName.toLowerCase() === 'svg') {
child.remove();
}
}
}
private _cacheChildrenWithExternalReferences(element: SVGElement) {
const elementsWithFuncIri = element.querySelectorAll(funcIriAttributeSelector);
const elements = (this._elementsWithExternalReferences = this._elementsWithExternalReferences || new Map());
elementsWithFuncIri.forEach(
(elementWithFuncIri) => {
funcIriAttributes.forEach(attr => {
const elementWithReference = elementWithFuncIri;
const value = elementWithReference.getAttribute(attr);
const match = value ? value.match(funcIriPattern) : null;
if (match) {
let attributes = elements.get(elementWithReference);
if (!attributes) {
attributes = [];
elements.set(elementWithReference, attributes);
}
attributes.push({name: attr, value: match[1]});
}
});
}
);
}
private _prependPathToReferences(path: string) {
const elements = this._elementsWithExternalReferences;
if (elements) {
elements.forEach((attrs, element) => {
attrs.forEach(attr => {
element.setAttribute(attr.name, `url('${path}#${attr.value}')`);
});
});
}
}
}

8
ui-ngx/src/app/shared/components/material-icon-select.component.html

@ -16,7 +16,7 @@
-->
<div *ngIf="!asBoxInput; else boxInput" fxLayout="row" [formGroup]="materialIconFormGroup">
<mat-icon class="icon-value" [ngStyle]="color ? { color: color } : {}" (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon>
<tb-icon class="icon-value" [ngStyle]="color ? { color: color } : {}" (click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</tb-icon>
<mat-form-field fxFlex>
<mat-label>{{ label }}</mat-label>
<input [required]="required" matInput formControlName="icon" (mousedown)="openIconDialog()">
@ -24,18 +24,18 @@
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
<tb-icon>close</tb-icon>
</button>
</mat-form-field>
</div>
<ng-template #boxInput>
<button type="button"
mat-stroked-button
class="icon-box"
class="tb-box-button"
[ngStyle]="color && !disabled ? { color: color } : {}"
[disabled]="disabled"
#matButton
(click)="openIconPopup($event, matButton)">
<mat-icon>{{materialIconFormGroup.get('icon').value}}</mat-icon>
<tb-icon matButtonIcon>{{materialIconFormGroup.get('icon').value}}</tb-icon>
</button>
</ng-template>

18
ui-ngx/src/app/shared/components/material-icon-select.component.scss

@ -24,21 +24,3 @@
}
}
}
:host ::ng-deep {
button.mat-mdc-button-base.icon-box {
width: 40px;
min-width: 40px;
height: 40px;
padding: 7px;
&:not(:disabled) {
color: rgba(0, 0, 0, 0.87);
}
> .mat-icon {
width: 24px;
height: 24px;
font-size: 24px;
margin: 0;
}
}
}

4
ui-ngx/src/app/shared/components/material-icons.component.html

@ -39,7 +39,7 @@
matTooltip="{{ icon.displayName }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon.name}}</mat-icon>
<tb-icon matButtonIcon>{{icon.name}}</tb-icon>
</button>
<button *ngIf="icon.name !== selectedIcon"
class="tb-select-icon-button"
@ -48,7 +48,7 @@
matTooltip="{{ icon.displayName }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon.name}}</mat-icon>
<tb-icon matButtonIcon>{{icon.name}}</tb-icon>
</button>
</ng-container>
</div>

8
ui-ngx/src/app/shared/components/notification/notification.component.html

@ -18,14 +18,14 @@
<section fxLayout="row" fxLayoutAlign="space-between start" class="notification"
[ngStyle]="{borderColor: notificationColor()}">
<div *ngIf="showIcon; else defaultIcon">
<mat-icon class="icon" [ngStyle]="{color: notification.additionalConfig.icon.color}">
<tb-icon class="icon" [ngStyle]="{color: notification.additionalConfig.icon.color}">
{{ notification.additionalConfig.icon.icon }}
</mat-icon>
</tb-icon>
</div>
<ng-template #defaultIcon>
<mat-icon class="icon" *ngIf="notificationTypeIcons.get(notification.type)" [ngStyle]="notificationIconColor()">
<tb-icon class="icon" *ngIf="notificationTypeIcons.get(notification.type)" [ngStyle]="notificationIconColor()">
{{ notificationTypeIcons.get(notification.type) }}
</mat-icon>
</tb-icon>
</ng-template>
<div class="content" fxFlex>
<div class="title" [innerHTML]="(title | safe: 'html' )"></div>

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

@ -27,3 +27,4 @@ export * from './toggle-header.component';
export * from './toggle-select.component';
export * from './unit-input.component';
export * from './material-icons.component';
export * from './icon.component';

2
ui-ngx/src/app/shared/components/unit-input.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field appearance="outline" class="tb-inline-field tb-suffix-show-on-hover" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="tb-inline-field tb-suffix-show-on-hover" subscriptSizing="dynamic" style="width: 100%;">
<input matInput #unitInput [formControl]="unitsFormControl"
placeholder="{{ 'widget-config.set' | translate }}"
(focusin)="onFocus()"

63
ui-ngx/src/app/shared/models/icon.models.ts

@ -19,6 +19,66 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { isNotEmptyStr } from '@core/utils';
export const svgIcons: {[key: string]: string} = {
'google-logo': '<svg viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 ' +
'2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" ' +
'd="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 ' +
'7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 ' +
'16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 ' +
'15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 ' +
'24 48z"/><path fill="none" d="M0 0h48v48H0z"/></svg>',
'github-logo': '<svg viewBox="0 0 32.7 32.7"><path d="M16.3 0C7.3 0 0 7.3 0 16.3c0 7.2 4.7 13.3 11.1 15.5.8.1 ' +
'1.1-.4 1.1-.8v-2.8c-4.5 1-5.5-2.2-5.5-2.2-.7-1.9-1.8-2.4-1.8-2.4-1.5-1 .1-1 .1-1 1.6.1 2.5 1.7 2.5 1.7 1.5 ' +
'2.5 3.8 1.8 4.7 1.4.1-1.1.6-1.8 1-2.2-3.6-.4-7.4-1.8-7.4-8.1 0-1.8.6-3.2 1.7-4.4-.2-.4-.7-2.1.2-4.3 0 0 ' +
'1.4-.4 4.5 1.7 1.3-.4 2.7-.5 4.1-.5s2.8.2 4.1.5c3.1-2.1 4.5-1.7 4.5-1.7.9 2.2.3 3.9.2 4.3 1 1.1 1.7 2.6 ' +
'1.7 4.4 0 6.3-3.8 7.6-7.4 8 .6.5 1.1 1.5 1.1 3v4.5c0 .4.3.9 1.1.8 6.5-2.2 11.1-8.3 11.1-15.5C32.6 7.3 ' +
'25.3 0 16.3 0z" fill="#211c19"/></svg>',
'facebook-logo': '<svg viewBox="0 0 263 263"><path d="M263 131.5C263 58.9 204.1 0 131.5 0S0 58.9 0 131.5c0 ' +
'65.6 48.1 120 110.9 129.9v-91.9H77.5v-38h33.4v-29c0-33 19.6-51.2 49.7-51.2 14.4 0 29.4 2.6 29.4 ' +
'2.6v32.4h-16.5c-16.3 0-21.4 10.1-21.4 20.5v24.7h36.4l-5.8 38h-30.6v91.9c62.8-9.9 110.9-64.3 110.9-129.9z" ' +
'fill="#1877f2"/><path d="M182.7 169.5l5.8-38H152v-24.7c0-10.4 5.1-20.5 21.4-20.5H190V53.9s-15-2.6-29.4-2.6c-30 ' +
'0-49.7 18.2-49.7 51.2v29H77.5v38h33.4v91.9c6.7 1.1 13.6 1.6 20.5 1.6s13.9-.5 20.5-1.6v-91.9h30.8z" fill="#fff"/></svg>',
'apple-logo': '<svg viewBox="0 0 256 315"><path d="M213.803394,167.030943 C214.2452,214.609646 255.542482,230.442639 ' +
'256,230.644727 C255.650812,231.761357 249.401383,253.208293 234.24263,275.361446 C221.138555,294.513969 ' +
'207.538253,313.596333 186.113759,313.991545 C165.062051,314.379442 158.292752,301.507828 134.22469,301.507828 ' +
'C110.163898,301.507828 102.642899,313.596301 82.7151126,314.379442 C62.0350407,315.16201 46.2873831,293.668525 ' +
'33.0744079,274.586162 C6.07529317,235.552544 -14.5576169,164.286328 13.147166,116.18047 C26.9103111,92.2909053 ' +
'51.5060917,77.1630356 78.2026125,76.7751096 C98.5099145,76.3877456 117.677594,90.4371851 130.091705,90.4371851 ' +
'C142.497945,90.4371851 165.790755,73.5415029 190.277627,76.0228474 C200.528668,76.4495055 229.303509,80.1636878 ' +
'247.780625,107.209389 C246.291825,108.132333 213.44635,127.253405 213.803394,167.030988 M174.239142,50.1987033 ' +
'C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 ' +
'144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 ' +
'163.259413,63.4970262 174.239142,50.1987249" fill="#000000"></path></svg>',
'queues-list': '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">' +
'<path fill="#fff" d="M9 4V2H4a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h5v-2H4V4h5z"/>' +
'<path fill="#fff" d="M7 18V6h2v12H7zM11 6v12h2V6h-2zM15 20v2h5a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-5v2h5v16h-5z"/>' +
'<path fill="#fff" d="M15 18V6h2v12h-2z"/>' +
'</svg>'
};
const svgIconNamespaces: string[] = ['mdi'];
const svgIconNames = Object.keys(svgIcons);
export const splitIconName = (iconName: string): [string, string] => {
if (!iconName) {
return ['', ''];
}
const parts = iconName.split(':');
switch (parts.length) {
case 1:
return ['', parts[0]];
case 2:
return parts as [string, string];
default:
throw Error(`Invalid icon name: "${iconName}"`);
}
};
export const isSvgIcon = (icon: string): boolean => {
const [namespace, iconName] = splitIconName(icon);
return svgIconNamespaces.includes(namespace) || svgIconNames.includes(iconName);
};
export interface MaterialIcon {
name: string;
displayName?: string;
@ -43,7 +103,8 @@ export const getMaterialIcons = (resourcesService: ResourcesService, chunkSize
resourcesService.loadJsonResource<Array<MaterialIcon>>('/assets/metadata/material-icons.json',
(icons) => {
for (const icon of icons) {
const words = icon.name.replace(/_/g, ' ').split(' ');
const iconName = splitIconName(icon.name)[1];
const words = iconName.replace(/[_\-]/g, ' ').split(' ');
for (let i = 0; i < words.length; i++) {
words[i] = words[i].charAt(0).toUpperCase() + words[i].slice(1);
}

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

@ -57,7 +57,6 @@ export interface WidgetTypeTemplate {
export interface WidgetTypeData {
name: string;
icon: string;
isMdiIcon?: boolean;
configHelpLinkId: string;
template: WidgetTypeTemplate;
}
@ -94,7 +93,6 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
name: 'widget.rpc',
icon: 'mdi:developer-board',
configHelpLinkId: 'widgetsConfigRpc',
isMdiIcon: true,
template: {
bundleAlias: 'gpio_widgets',
alias: 'basic_gpio_control'
@ -182,6 +180,8 @@ export interface WidgetTypeParameters {
warnOnPageDataOverflow?: boolean;
ignoreDataUpdateOnIntervalTick?: boolean;
processNoDataByWidget?: boolean;
previewWidth?: string;
previewHeight?: string;
}
export interface WidgetControllerDescriptor {

8
ui-ngx/src/app/shared/pipe/date-ago.pipe.ts

@ -37,19 +37,21 @@ export class DateAgoPipe implements PipeTransform {
}
transform(value: number, args?: any): string {
transform(value: string| number | Date, args?: any): string {
if (value) {
const applyAgo = !!args?.applyAgo;
const short = !!args?.short;
const textPart = !!args?.textPart;
const ms = Math.floor((+new Date() - +new Date(value)));
if (ms < 29 * SECOND) { // less than 30 seconds ago will show as 'Just now'
return this.translate.instant('timewindow.just-now');
return this.translate.instant(textPart ? 'timewindow.just-now-lower' : 'timewindow.just-now');
}
let counter;
// eslint-disable-next-line guard-for-in
for (const i in intervals) {
counter = Math.floor(ms / intervals[i]);
if (counter > 0) {
let res = this.translate.instant(`timewindow.${i}`, {[i]: counter});
let res = this.translate.instant(`timewindow.${i+(short ? '-short' : '')}`, {[i]: counter});
if (applyAgo) {
res += ' ' + this.translate.instant('timewindow.ago');
}

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

@ -197,6 +197,7 @@ import { ToggleSelectComponent } from '@shared/components/toggle-select.componen
import { UnitInputComponent } from '@shared/components/unit-input.component';
import { MaterialIconsComponent } from '@shared/components/material-icons.component';
import { ColorPickerPanelComponent } from '@shared/components/color-picker/color-picker-panel.component';
import { TbIconComponent } from '@shared/components/icon.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -373,7 +374,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ToggleSelectComponent,
UnitInputComponent,
MaterialIconsComponent,
RuleChainSelectComponent
RuleChainSelectComponent,
TbIconComponent
],
imports: [
CommonModule,
@ -606,7 +608,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ToggleSelectComponent,
UnitInputComponent,
MaterialIconsComponent,
RuleChainSelectComponent
RuleChainSelectComponent,
TbIconComponent
]
})
export class SharedModule { }

88
ui-ngx/src/assets/help/en_US/date/date-format.md

@ -0,0 +1,88 @@
#### Pre-defined format options
| Option | Equivalent to | Examples (given in `en-US` locale) |
|------------|---------------------------------|-----------------------------------------------|
| short | M/d/yy, h:mm a | 6/15/15, 9:03 AM |
| medium | MMM d, y, h:mm:ss a | Jun 15, 2015, 9:03:01 AM |
| long | MMMM d, y, h:mm:ss a z | June 15, 2015 at 9:03:01 AM GMT+1 |
| full | EEEE, MMMM d, y, h:mm:ss a zzzz | Monday, June 15, 2015 at 9:03:01 AM GMT+01:00 |
| shortDate | M/d/yy | 6/15/15 |
| mediumDate | MMM d, y | Jun 15, 2015 |
| longDate | MMMM d, y | June 15, 2015 |
| fullDate | EEEE, MMMM d, y | Monday, June 15, 2015 |
| shortTime | h:mm a | 9:03 AM |
| mediumTime | h:mm:ss a | 9:03:01 AM |
| longTime | h:mm:ss a z | 9:03:01 AM GMT+1 |
| fullTime | h:mm:ss a zzzz | 9:03:01 AM GMT+01:00 |
#### Custom format options
You can construct a format string using symbols to specify the components
of a date-time value, as described in the following table.
Format details depend on the locale.
Fields marked with (*) are only available in the extra data set for the given locale.
| Field type | Format | Description | Example Value |
|---------------------|-------------|--------------------------------------------------------------|------------------------------------------------------------|
| Era | G, GG & GGG | Abbreviated | AD |
| | GGGG | Wide | Anno Domini |
| | GGGGG | Narrow | A |
| Year | y | Numeric: minimum digits | 2, 20, 201, 2017, 20173 |
| | yy | Numeric: 2 digits + zero padded | 02, 20, 01, 17, 73 |
| | yyy | Numeric: 3 digits + zero padded | 002, 020, 201, 2017, 20173 |
| | yyyy | Numeric: 4 digits or more + zero padded | 0002, 0020, 0201, 2017, 20173 |
| Week-numbering year | Y | Numeric: minimum digits | 2, 20, 201, 2017, 20173 |
| | YY | Numeric: 2 digits + zero padded | 02, 20, 01, 17, 73 |
| | YYY | Numeric: 3 digits + zero padded | 002, 020, 201, 2017, 20173 |
| | YYYY | Numeric: 4 digits or more + zero padded | 0002, 0020, 0201, 2017, 20173 |
| Month | M | Numeric: 1 digit | 9, 12 |
| | MM | Numeric: 2 digits + zero padded | 09, 12 |
| | MMM | Abbreviated | Sep |
| | MMMM | Wide | September |
| | MMMMM | Narrow | S |
| Month standalone | L | Numeric: 1 digit | 9, 12 |
| | LL | Numeric: 2 digits + zero padded | 09, 12 |
| | LLL | Abbreviated | Sep |
| | LLLL | Wide | September |
| | LLLLL | Narrow | S |
| Week of year | w | Numeric: minimum digits | 1... 53 |
| | ww | Numeric: 2 digits + zero padded | 01... 53 |
| Week of month | W | Numeric: 1 digit | 1... 5 |
| Day of month | d | Numeric: minimum digits | 1 |
| | dd | Numeric: 2 digits + zero padded | 01 |
| Week day | E, EE & EEE | Abbreviated | Tue |
| | EEEE | Wide | Tuesday |
| | EEEEE | Narrow | T |
| | EEEEEE | Short | Tu |
| Week day standalone | c, cc | Numeric: 1 digit | 2 |
| | ccc | Abbreviated | Tue |
| | cccc | Wide | Tuesday |
| | ccccc | Narrow | T |
| | cccccc | Short | Tu |
| Period | a, aa & aaa | Abbreviated | am/pm or AM/PM |
| | aaaa | Wide (fallback to `a` when missing) | ante meridiem/post meridiem |
| | aaaaa | Narrow | a/p |
| Period* | B, BB & BBB | Abbreviated | mid. |
| | BBBB | Wide | am, pm, midnight, noon, morning, afternoon, evening, night |
| | BBBBB | Narrow | md |
| Period standalone* | b, bb & bbb | Abbreviated | mid. |
| | bbbb | Wide | am, pm, midnight, noon, morning, afternoon, evening, night |
| | bbbbb | Narrow | md |
| Hour 1-12 | h | Numeric: minimum digits | 1, 12 |
| | hh | Numeric: 2 digits + zero padded | 01, 12 |
| Hour 0-23 | H | Numeric: minimum digits | 0, 23 |
| | HH | Numeric: 2 digits + zero padded | 00, 23 |
| Minute | m | Numeric: minimum digits | 8, 59 |
| | mm | Numeric: 2 digits + zero padded | 08, 59 |
| Second | s | Numeric: minimum digits | 0... 59 |
| | ss | Numeric: 2 digits + zero padded | 00... 59 |
| Fractional seconds | S | Numeric: 1 digit | 0... 9 |
| | SS | Numeric: 2 digits + zero padded | 00... 99 |
| | SSS | Numeric: 3 digits + zero padded (= milliseconds) | 000... 999 |
| Zone | z, zz & zzz | Short specific non location format (fallback to O) | GMT-8 |
| | zzzz | Long specific non location format (fallback to OOOO) | GMT-08:00 |
| | Z, ZZ & ZZZ | ISO8601 basic format | -0800 |
| | ZZZZ | Long localized GMT format | GMT-8:00 |
| | ZZZZZ | ISO8601 extended format + Z indicator for offset 0 (= XXXXX) | -08:00 |
| | O, OO & OOO | Short localized GMT format | GMT-8 |
| | OOOO | Long localized GMT format | GMT-08:00 |

40
ui-ngx/src/assets/help/en_US/widget/lib/card/value_color_fn.md

@ -0,0 +1,40 @@
#### Color function
<div class="divider"></div>
<br/>
*function (value): string*
A JavaScript function used to compute a color.
**Parameters:**
<ul>
<li><b>value:</b> <code>primitive (number/string/boolean)</code> - A value of the current datapoint.
</li>
</ul>
**Returns:**
Should return string value presenting color.
In case no data is returned, color value from **Color** settings field will be used.
<div class="divider"></div>
##### Examples
* Calculate color depending on `temperature` telemetry value:
```javascript
var temperature = value;
if (typeof temperature !== undefined) {
var percent = (temperature + 60)/120 * 100;
return tinycolor.mix('blue', 'red', percent).toHexString();
}
return 'blue';
{:copy-code}
```
<br>
<br>

2
ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md

@ -31,7 +31,7 @@ if (type == 'colorpin') {
var temperature = data['temperature'];
if (typeof temperature !== undefined) {
var percent = (temperature + 60)/120 * 100;
return tinycolor.mix('blue', 'red', amount = percent).toHexString();
return tinycolor.mix('blue', 'red', percent).toHexString();
}
return 'blue';
}

2
ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md

@ -31,7 +31,7 @@ if (type == 'colorpin') {
var temperature = data['temperature'];
if (typeof temperature !== undefined) {
var percent = (temperature + 60)/120 * 100;
return tinycolor.mix('blue', 'red', amount = percent).toHexString();
return tinycolor.mix('blue', 'red', percent).toHexString();
}
return 'blue';
}

2
ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md

@ -31,7 +31,7 @@ if (type == 'colorpin') {
var temperature = data['temperature'];
if (typeof temperature !== undefined) {
var percent = (temperature + 60)/120 * 100;
return tinycolor.mix('blue', 'red', amount = percent).toHexString();
return tinycolor.mix('blue', 'red', percent).toHexString();
}
return 'blue';
}

2
ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_color_fn.md

@ -31,7 +31,7 @@ if (type == 'thermostat') {
var temperature = data['temperature'];
if (typeof temperature !== undefined) {
var percent = (temperature + 60)/120 * 100;
return tinycolor.mix('blue', 'red', amount = percent).toHexString();
return tinycolor.mix('blue', 'red', percent).toHexString();
}
return 'blue';
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save