Browse Source

UI: Add value card widget. Introduce tb-icon component to handle both font and svg (mdi) icons.

pull/8997/head
Igor Kulikov 3 years ago
parent
commit
9d8a9943bf
  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. 59
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-card-basic-config.component.html
  18. 254
      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. 263
      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. 131
      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. 95
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
  45. 51
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.scss
  46. 136
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.ts
  47. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.html
  48. 102
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings.component.ts
  49. 43
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.html
  50. 116
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.scss
  51. 190
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/image-cards-select.component.ts
  52. 26
      ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts
  53. 6
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  54. 7
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  55. 14
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.html
  56. 2
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  57. 7
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  58. 2
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.html
  59. 9
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.scss
  60. 6
      ui-ngx/src/app/modules/home/components/widget/widget-preview.component.ts
  61. 3
      ui-ngx/src/app/modules/home/menu/menu-link.component.html
  62. 3
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.html
  63. 3
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  64. 1
      ui-ngx/src/app/modules/home/models/entity/entities-table-config.models.ts
  65. 1
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  66. 3
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  67. 3
      ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html
  68. 2
      ui-ngx/src/app/modules/home/pages/home-links/home-links.component.scss
  69. 8
      ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.html
  70. 2
      ui-ngx/src/app/modules/home/pages/widget/select-widget-type-dialog.component.scss
  71. 6
      ui-ngx/src/app/shared/components/breadcrumb.component.html
  72. 2
      ui-ngx/src/app/shared/components/breadcrumb.component.ts
  73. 1
      ui-ngx/src/app/shared/components/breadcrumb.ts
  74. 4
      ui-ngx/src/app/shared/components/color-input.component.html
  75. 6
      ui-ngx/src/app/shared/components/color-input.component.scss
  76. 281
      ui-ngx/src/app/shared/components/icon.component.ts
  77. 8
      ui-ngx/src/app/shared/components/material-icon-select.component.html
  78. 18
      ui-ngx/src/app/shared/components/material-icon-select.component.scss
  79. 4
      ui-ngx/src/app/shared/components/material-icons.component.html
  80. 8
      ui-ngx/src/app/shared/components/notification/notification.component.html
  81. 1
      ui-ngx/src/app/shared/components/public-api.ts
  82. 63
      ui-ngx/src/app/shared/models/icon.models.ts
  83. 4
      ui-ngx/src/app/shared/models/widget.models.ts
  84. 7
      ui-ngx/src/app/shared/shared.module.ts
  85. 40
      ui-ngx/src/assets/help/en_US/widget/lib/card/value_color_fn.md
  86. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/color_fn.md
  87. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/path_color_fn.md
  88. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/path_point_color_fn.md
  89. 2
      ui-ngx/src/assets/help/en_US/widget/lib/map/polygon_color_fn.md
  90. 25
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  91. 6368
      ui-ngx/src/assets/metadata/material-icons.json
  92. 21
      ui-ngx/src/assets/widget/value-card/centered-layout.svg
  93. 21
      ui-ngx/src/assets/widget/value-card/horizontal-layout.svg
  94. 21
      ui-ngx/src/assets/widget/value-card/horizontal-reversed-layout.svg
  95. 19
      ui-ngx/src/assets/widget/value-card/simplified-layout.svg
  96. 24
      ui-ngx/src/assets/widget/value-card/square-layout.svg
  97. 20
      ui-ngx/src/assets/widget/value-card/vertical-layout.svg
  98. 42
      ui-ngx/src/form.scss
  99. 26
      ui-ngx/src/styles.scss

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

@ -125,7 +125,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) {

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

@ -0,0 +1,59 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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>
</ng-container>

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

@ -0,0 +1,254 @@
///
/// 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 } 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 { isDefinedAndNotNull, isUndefined } from '@core/utils';
import { 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;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private cd: ChangeDetectorRef,
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');
}
}

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"

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

@ -0,0 +1,263 @@
///
/// 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';
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 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">{{ dateText }}</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,
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;
dateText = '';
dateStyle: ComponentStyle = {};
dateColor: ColorProcessor;
backgroundStyle: ComponentStyle = {};
overlayStyle: ComponentStyle = {};
private horizontal = false;
private dateFormat: string;
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 = 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 value;
if (tsValue) {
value = tsValue[1];
this.valueText = formatValue(value, this.decimals, this.units, true);
this.dateText = this.date.transform(tsValue[0], this.dateFormat);
} else {
this.valueText = 'N/A';
this.dateText = '';
}
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,
Font
} 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: string;
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: 'yyyy-MM-dd HH:mm:ss',
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;
}
}

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

@ -0,0 +1,131 @@
///
/// 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);
setTimeout(() => {this.popover?.updatePosition();}, 0);
}
addRange() {
const newRange: ColorRange = {
color: 'rgba(0,0,0,0.87)'
};
this.rangeListFormArray.push(this.colorRangeControl(newRange), {emitEvent: true});
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 = {};
}
}
}

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

@ -0,0 +1,95 @@
<!--
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>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="sizeUnit">
<mat-option *ngFor="let cssUnit of cssUnitsList" [value]="cssUnit">{{ cssUnit }}</mat-option>
</mat-select>
</mat-form-field>
</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;
}
}

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

@ -0,0 +1,136 @@
///
/// 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,
cssUnits,
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;
cssUnitsList = cssUnits;
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});
}
}

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

@ -266,6 +266,16 @@ 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';
@NgModule({
declarations: [
@ -367,7 +377,13 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent
QuickLinksWidgetSettingsComponent,
ImageCardsSelectOptionDirective,
ImageCardsSelectComponent,
FontSettingsComponent,
FontSettingsPanelComponent,
ColorSettingsComponent,
ColorSettingsPanelComponent
],
imports: [
CommonModule,
@ -473,7 +489,13 @@ import { LegendConfigComponent } from '@home/components/widget/lib/settings/comm
RouteMapWidgetSettingsComponent,
TripAnimationWidgetSettingsComponent,
DocLinksWidgetSettingsComponent,
QuickLinksWidgetSettingsComponent
QuickLinksWidgetSettingsComponent,
ImageCardsSelectOptionDirective,
ImageCardsSelectComponent,
FontSettingsComponent,
FontSettingsPanelComponent,
ColorSettingsComponent,
ColorSettingsPanelComponent
]
})
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;
}
}

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

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 {

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

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

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

@ -4407,6 +4407,17 @@
"ticks": "Ticks",
"horizontal-axis": "Horizontal axis"
},
"color": {
"color-settings": "Color settings",
"color-type-constant": "Constant",
"color-type-range": "Range",
"color-type-function": "Function",
"color": "Color",
"value-range": "Value range",
"from": "From",
"to": "To",
"color-function": "Color function"
},
"dashboard-state": {
"dashboard-state-settings": "Dashboard state settings",
"dashboard-state": "Dashboard state id",
@ -5216,6 +5227,16 @@
"label-position-left": "Left",
"label-position-top": "Top"
},
"value-card": {
"layout": "Layout",
"layout-square": "Square",
"layout-vertical": "Vertical",
"layout-centered": "Centered",
"layout-simplified": "Simplified",
"layout-horizontal": "Horizontal",
"layout-horizontal-reversed": "Horizontal reversed",
"label": "Label"
},
"table": {
"common-table-settings": "Common Table Settings",
"enable-search": "Enable search",
@ -5290,6 +5311,7 @@
"source-entity-attribute": "Source entity attribute"
},
"widget-font": {
"font-settings": "Font settings",
"font-family": "Font family",
"size": "Size",
"relative-font-size": "Relative font size (percents)",
@ -5303,7 +5325,8 @@
"font-weight-bolder": "Bolder",
"font-weight-lighter": "Lighter",
"color": "Color",
"shadow-color": "Shadow color"
"shadow-color": "Shadow color",
"preview": "Preview"
},
"home": {
"no-data-available": "No data available"

6368
ui-ngx/src/assets/metadata/material-icons.json

File diff suppressed because one or more lines are too long

21
ui-ngx/src/assets/widget/value-card/centered-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

21
ui-ngx/src/assets/widget/value-card/horizontal-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

21
ui-ngx/src/assets/widget/value-card/horizontal-reversed-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

19
ui-ngx/src/assets/widget/value-card/simplified-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

24
ui-ngx/src/assets/widget/value-card/square-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 31 KiB

20
ui-ngx/src/assets/widget/value-card/vertical-layout.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

42
ui-ngx/src/form.scss

@ -66,6 +66,7 @@
overflow: visible;
}
> .mat-expansion-panel-header {
user-select: none;
font-weight: 500;
font-size: 16px;
line-height: 24px;
@ -138,6 +139,13 @@
padding: 7px 7px 7px 16px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
&.no-border {
border: none;
border-radius: 0;
}
&.no-padding {
padding: 0;
}
&.same-padding {
padding-right: 16px;
}
@ -154,9 +162,18 @@
&.medium-width {
width: 220px;
}
@media #{$mat-xs} {
width: auto;
&.medium-width {
width: auto;
}
}
}
.fixed-title-width {
min-width: 200px;
@media #{$mat-xs} {
min-width: 0;
}
}
.mat-slide:only-child {
margin: 8px 0;
@ -193,7 +210,7 @@
}
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(.mdc-text-field--invalid):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
border-color: rgba(0, 0, 0, 0.12);
}
@ -416,4 +433,27 @@
line-height: 16px;
}
}
button.mat-mdc-button-base.tb-box-button {
width: 40px;
min-width: 40px;
height: 40px;
padding: 7px;
.mat-mdc-button-touch-target {
width: 40px;
height: 40px;
}
&:not(:disabled) {
color: rgba(0, 0, 0, 0.54);
}
&:disabled {
color: rgba(0, 0, 0, 0.12);
}
> .mat-icon {
width: 24px;
height: 24px;
font-size: 24px;
margin: 0;
}
}
}

26
ui-ngx/src/styles.scss

@ -624,7 +624,7 @@ mat-label {
.mat-toolbar.mat-primary {
button.mat-mdc-icon-button {
mat-icon {
.mat-icon {
color: white;
}
}
@ -648,11 +648,11 @@ mat-label {
mat-toolbar.mat-mdc-table-toolbar:not(.mat-primary), .mat-mdc-cell, .mat-expansion-panel-header {
button.mat-mdc-icon-button {
mat-icon {
.mat-icon {
color: rgba(0, 0, 0, .54);
}
&[disabled][disabled] {
mat-icon {
.mat-icon {
color: rgba(0, 0, 0, .26);
}
}
@ -791,7 +791,7 @@ mat-label {
&.mat-number-cell {
text-align: end;
}
mat-icon {
.mat-icon {
color: rgba(0, 0, 0, .54);
}
}
@ -951,7 +951,7 @@ mat-label {
padding: 0 6px;
min-width: 88px;
}
mat-icon {
.mat-icon {
margin-right: 5px;
}
}
@ -1051,7 +1051,7 @@ mat-label {
background: #ccc;
opacity: .85;
mat-icon {
.mat-icon {
color: #666;
}
}
@ -1094,7 +1094,17 @@ mat-label {
box-shadow: none;
border-radius: 4px;
.tb-color-result {
border: 1px solid rgba(0, 0, 0, 0.12);
position: relative;
&:after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.12);
}
}
&.disabled {
cursor: initial;
@ -1155,7 +1165,7 @@ mat-label {
.tb-drag-handle {
cursor: move;
mat-icon {
.mat-icon {
pointer-events: none;
}
}

Loading…
Cancel
Save