Browse Source

Merge branch 'master' into fix/4228-remove-redundant-name

pull/11457/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
2f7c5e41a1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1401
      application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg
  2. 1633
      application/src/main/data/json/system/scada_symbols/elevated-tank.svg
  3. 1407
      application/src/main/data/json/system/scada_symbols/horizontal-tank.svg
  4. 1392
      application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg
  5. 1453
      application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg
  6. 1455
      application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg
  7. 1394
      application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg
  8. 1056
      application/src/main/data/json/system/scada_symbols/pool.svg
  9. 1426
      application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg
  10. 1456
      application/src/main/data/json/system/scada_symbols/spherical-tank.svg
  11. 1462
      application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg
  12. 1468
      application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg
  13. 1435
      application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg
  14. 1455
      application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg
  15. 1374
      application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg
  16. 4
      application/src/main/data/json/system/scada_symbols/vertical-tank.svg
  17. 17
      application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json
  18. 6
      application/src/main/java/org/thingsboard/server/controller/AdminController.java
  19. 4
      application/src/main/resources/thingsboard.yml
  20. 6
      common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java
  21. 2
      common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java
  22. 3
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
  23. 1
      common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java
  24. 3
      msa/tb-node/docker/Dockerfile
  25. 3
      ui-ngx/src/app/core/http/widget.service.ts
  26. 15
      ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts
  27. 222
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  28. 81
      ui-ngx/src/app/core/services/item-buffer.service.ts
  29. 903
      ui-ngx/src/app/core/services/menu.models.ts
  30. 960
      ui-ngx/src/app/core/services/menu.service.ts
  31. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  32. 8
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.ts
  33. 57
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  34. 20
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss
  35. 247
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  36. 21
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts
  37. 43
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  38. 148
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts
  39. 13
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss
  40. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  41. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts
  42. 66
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html
  43. 85
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.ts
  44. 4
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html
  45. 67
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.ts
  46. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts
  47. 86
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html
  48. 58
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss
  49. 227
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts
  50. 29
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html
  51. 34
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.scss
  52. 81
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts
  53. 2
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  54. 31
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.ts
  55. 6
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  56. 16
      ui-ngx/src/app/modules/home/components/entity/entity-details-panel.component.ts
  57. 10
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  58. 2
      ui-ngx/src/app/modules/home/components/router-tabs.component.ts
  59. 3
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts
  60. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  61. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts
  62. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  63. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  64. 21
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html
  65. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts
  66. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts
  67. 78
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html
  68. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts
  69. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  70. 11
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/quick-link.component.ts
  71. 2
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.html
  72. 10
      ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts
  73. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html
  74. 2
      ui-ngx/src/app/modules/home/components/widget/widget-component.service.ts
  75. 4
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  76. 4
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  77. 16
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  78. 30
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss
  79. 93
      ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts
  80. 2
      ui-ngx/src/app/modules/home/menu/menu-link.component.html
  81. 2
      ui-ngx/src/app/modules/home/menu/menu-toggle.component.html
  82. 4
      ui-ngx/src/app/modules/home/models/dashboard-component.models.ts
  83. 11
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  84. 52
      ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts
  85. 4
      ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts
  86. 4
      ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts
  87. 4
      ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts
  88. 4
      ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts
  89. 4
      ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts
  90. 4
      ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts
  91. 4
      ui-ngx/src/app/modules/home/pages/dashboard/dashboard-routing.module.ts
  92. 4
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts
  93. 4
      ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts
  94. 10
      ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts
  95. 4
      ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts
  96. 4
      ui-ngx/src/app/modules/home/pages/features/features-routing.module.ts
  97. 4
      ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts
  98. 2
      ui-ngx/src/app/modules/home/pages/home-links/home-links.component.html
  99. 19
      ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts
  100. 1
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

1401
application/src/main/data/json/system/scada_symbols/cylindrical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 112 KiB

1633
application/src/main/data/json/system/scada_symbols/elevated-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 138 KiB

1407
application/src/main/data/json/system/scada_symbols/horizontal-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 114 KiB

1392
application/src/main/data/json/system/scada_symbols/large-cylindrical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 112 KiB

1453
application/src/main/data/json/system/scada_symbols/large-stand-cylindrical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 119 KiB

1455
application/src/main/data/json/system/scada_symbols/large-stand-vertical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 120 KiB

1394
application/src/main/data/json/system/scada_symbols/large-vertical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 113 KiB

1056
application/src/main/data/json/system/scada_symbols/pool.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 73 KiB

1426
application/src/main/data/json/system/scada_symbols/small-spherical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 104 KiB

1456
application/src/main/data/json/system/scada_symbols/spherical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 118 KiB

1462
application/src/main/data/json/system/scada_symbols/stand-cylindrical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 120 KiB

1468
application/src/main/data/json/system/scada_symbols/stand-horizontal-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 121 KiB

1435
application/src/main/data/json/system/scada_symbols/stand-vertical-short-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 108 KiB

1455
application/src/main/data/json/system/scada_symbols/stand-vertical-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 120 KiB

1374
application/src/main/data/json/system/scada_symbols/vertical-short-tank.svg

File diff suppressed because it is too large

After

Width:  |  Height:  |  Size: 101 KiB

4
application/src/main/data/json/system/scada_symbols/vertical-tank.svg

@ -311,7 +311,7 @@
"id": "valueBoxColor", "id": "valueBoxColor",
"name": "{i18n:scada.symbol.value-box}", "name": "{i18n:scada.symbol.value-box}",
"type": "color", "type": "color",
"default": "#FFFFFF", "default": "#F3F3F3",
"required": null, "required": null,
"subLabel": null, "subLabel": null,
"divider": null, "divider": null,
@ -633,7 +633,7 @@
</g><mask id="path-215-inside-2_1687_130892" fill="#ffffff"> </g><mask id="path-215-inside-2_1687_130892" fill="#ffffff">
<path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z"/> <path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z"/>
</mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z" fill="#d9d9d9"/><path d="m7 170h586v-6h-586zm586 8h-586v6h586zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10zm-590 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4z" fill="#727171" mask="url(#path-215-inside-2_1687_130892)"/><g filter="url(#filter0_ii_1687_130892)" tb:tag="value-box"> </mask><path d="m7 181c-3.866 0-7-3.134-7-7s3.134-7 7-7h586c3.866 0 7 3.134 7 7s-3.134 7-7 7z" fill="#d9d9d9"/><path d="m7 170h586v-6h-586zm586 8h-586v6h586zm4-4c0 2.209-1.791 4-4 4v6c5.523 0 10-4.477 10-10zm-4-4c2.209 0 4 1.791 4 4h6c0-5.523-4.477-10-10-10zm-590 4c0-2.209 1.7909-4 4-4v-6c-5.5228 0-10 4.477-10 10zm-6 0c0 5.523 4.4771 10 10 10v-6c-2.2091 0-4-1.791-4-4z" fill="#727171" mask="url(#path-215-inside-2_1687_130892)"/><g filter="url(#filter0_ii_1687_130892)" tb:tag="value-box">
<path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#fff" fill-opacity=".75" tb:tag="value-box-background"/> <path d="m180 62c0-6.6274 5.373-12 12-12h216c6.627 0 12 5.3726 12 12v56c0 6.627-5.373 12-12 12h-216c-6.627 0-12-5.373-12-12z" fill="#f3f3f3" tb:tag="value-box-background"/>
<path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/> <path d="m192 51.5h216c5.799 0 10.5 4.701 10.5 10.5v56c0 5.799-4.701 10.5-10.5 10.5h-216c-5.799 0-10.5-4.701-10.5-10.5v-56c0-5.799 4.701-10.5 10.5-10.5z" stroke="#727171" stroke-width="3"/>
<text x="301.5625" y="93.546875" dominant-baseline="middle" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan>1660 gal</tspan></text> <text x="301.5625" y="93.546875" dominant-baseline="middle" fill="#727171" font-family="Roboto" font-size="32px" font-weight="500" text-anchor="middle" tb:tag="value-text" xml:space="preserve"><tspan>1660 gal</tspan></text>
</g><defs> </g><defs>

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

17
application/src/main/data/json/system/widget_bundles/scada_water_system_symbols.json

@ -47,6 +47,21 @@
"vertical_wheel_valve", "vertical_wheel_valve",
"horizontal_ball_valve", "horizontal_ball_valve",
"vertical_ball_valve", "vertical_ball_valve",
"vertical_tank" "vertical_tank",
"stand_vertical_tank",
"cylindrical_tank",
"stand_cylindrical_tank",
"vertical_short_tank",
"stand_vertical_short_tank",
"large_cylindrical_tank",
"large_stand_cylindrical_tank",
"large_vertical_tank",
"large_stand_vertical_tank",
"horizontal_tank",
"stand_horizontal_tank",
"spherical_tank",
"small_spherical_tank",
"elevated_tank",
"pool"
] ]
} }

6
application/src/main/java/org/thingsboard/server/controller/AdminController.java

@ -137,7 +137,7 @@ public class AdminController extends BaseController {
return adminSettings; return adminSettings;
} }
@ApiOperation(value = "Get the Administration Settings object using key (getAdminSettings)", @ApiOperation(value = "Creates or Updates the Administration Settings (saveAdminSettings)",
notes = "Creates or Updates the Administration Settings. Platform generates random Administration Settings Id during settings creation. " + notes = "Creates or Updates the Administration Settings. Platform generates random Administration Settings Id during settings creation. " +
"The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " + "The Administration Settings Id will be present in the response. Specify the Administration Settings Id when you would like to update the Administration Settings. " +
"Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH) "Referencing non-existing Administration Settings Id will cause an error." + SYSTEM_AUTHORITY_PARAGRAPH)
@ -160,7 +160,7 @@ public class AdminController extends BaseController {
return adminSettings; return adminSettings;
} }
@ApiOperation(value = "Get the Security Settings object", @ApiOperation(value = "Get the Security Settings object (getSecuritySettings)",
notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH) notes = "Get the Security Settings object that contains password policy, etc." + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")
@RequestMapping(value = "/securitySettings", method = RequestMethod.GET) @RequestMapping(value = "/securitySettings", method = RequestMethod.GET)
@ -237,7 +237,7 @@ public class AdminController extends BaseController {
} }
} }
@ApiOperation(value = "Send test sms (sendTestMail)", @ApiOperation(value = "Send test sms (sendTestSms)",
notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. " notes = "Attempts to send test sms to the System Administrator User using SMS Settings and phone number provided as a parameters of the request. "
+ SYSTEM_AUTHORITY_PARAGRAPH) + SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')") @PreAuthorize("hasAuthority('SYS_ADMIN')")

4
application/src/main/resources/thingsboard.yml

@ -20,8 +20,8 @@ server:
address: "${HTTP_BIND_ADDRESS:0.0.0.0}" address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port # Server bind port
port: "${HTTP_BIND_PORT:8080}" port: "${HTTP_BIND_PORT:8080}"
# Server forward headers strategy # Server forward headers strategy. Required for SWAGGER UI when reverse proxy is used
forward_headers_strategy: "${HTTP_FORWARD_HEADERS_STRATEGY:NONE}" forward_headers_strategy: "${HTTP_FORWARD_HEADERS_STRATEGY:framework}"
# Server SSL configuration # Server SSL configuration
ssl: ssl:
# Enable/disable SSL support # Enable/disable SSL support

6
common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java

@ -31,11 +31,11 @@ import java.util.List;
@NoArgsConstructor @NoArgsConstructor
public class AttributesEntityView implements Serializable { public class AttributesEntityView implements Serializable {
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of client-side attribute keys to expose", example = "currentConfiguration") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of client-side attribute keys to expose", example = "[\"currentConfiguration\"]")
private List<String> cs = new ArrayList<>(); private List<String> cs = new ArrayList<>();
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of server-side attribute keys to expose", example = "model") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of server-side attribute keys to expose", example = "[\"model\"]")
private List<String> ss = new ArrayList<>(); private List<String> ss = new ArrayList<>();
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of shared attribute keys to expose", example = "targetConfiguration") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of shared attribute keys to expose", example = "[\"targetConfiguration\"]")
private List<String> sh = new ArrayList<>(); private List<String> sh = new ArrayList<>();
public AttributesEntityView(List<String> cs, public AttributesEntityView(List<String> cs,

2
common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java

@ -31,7 +31,7 @@ import java.util.List;
@NoArgsConstructor @NoArgsConstructor
public class TelemetryEntityView implements Serializable { public class TelemetryEntityView implements Serializable {
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of time-series data keys to expose", example = "temperature, humidity") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "List of time-series data keys to expose", example = "[\"temperature\", \"humidity\"]")
private List<String> timeseries; private List<String> timeseries;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with attributes to expose") @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with attributes to expose")
private AttributesEntityView attributes; private AttributesEntityView attributes;

3
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java

@ -151,6 +151,7 @@ public class TbKafkaSettings {
Properties props = toProps(); Properties props = toProps();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPollRecords); props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPollRecords);
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, sessionTimeoutMs);
props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, maxPartitionFetchBytes); props.put(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG, maxPartitionFetchBytes);
props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes); props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes);
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs); props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs);
@ -193,8 +194,6 @@ public class TbKafkaSettings {
} }
props.put(CommonClientConfigs.REQUEST_TIMEOUT_MS_CONFIG, requestTimeoutMs); props.put(CommonClientConfigs.REQUEST_TIMEOUT_MS_CONFIG, requestTimeoutMs);
props.put(CommonClientConfigs.SESSION_TIMEOUT_MS_CONFIG, sessionTimeoutMs);
props.putAll(PropertyUtils.getProps(otherInline)); props.putAll(PropertyUtils.getProps(otherInline));
if (other != null) { if (other != null) {

1
common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java

@ -49,7 +49,6 @@ class TbKafkaSettingsTest {
Properties props = settings.toProps(); Properties props = settings.toProps();
assertThat(props).as("TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS").containsEntry("request.timeout.ms", 30000); assertThat(props).as("TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS").containsEntry("request.timeout.ms", 30000);
assertThat(props).as("TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS").containsEntry("session.timeout.ms", 10000);
//other-inline //other-inline
assertThat(props).as("metrics.recording.level").containsEntry("metrics.recording.level", "INFO"); assertThat(props).as("metrics.recording.level").containsEntry("metrics.recording.level", "INFO");

3
msa/tb-node/docker/Dockerfile

@ -18,9 +18,6 @@ FROM thingsboard/openjdk17:bookworm-slim
COPY start-tb-node.sh ${pkg.name}.deb /tmp/ COPY start-tb-node.sh ${pkg.name}.deb /tmp/
# Required for SWAGGER UI when reverse proxy is used
ENV HTTP_FORWARD_HEADERS_STRATEGY=framework
RUN chmod a+x /tmp/*.sh \ RUN chmod a+x /tmp/*.sh \
&& mv /tmp/start-tb-node.sh /usr/bin && \ && mv /tmp/start-tb-node.sh /usr/bin && \
(yes | dpkg -i /tmp/${pkg.name}.deb) && \ (yes | dpkg -i /tmp/${pkg.name}.deb) && \

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

@ -185,8 +185,9 @@ export class WidgetService {
public saveWidgetTypeDetails(widgetInfo: WidgetInfo, public saveWidgetTypeDetails(widgetInfo: WidgetInfo,
id: WidgetTypeId, id: WidgetTypeId,
createdTime: number, createdTime: number,
version: number,
config?: RequestConfig): Observable<WidgetTypeDetails> { config?: RequestConfig): Observable<WidgetTypeDetails> {
const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, createdTime); const widgetTypeDetails = toWidgetTypeDetails(widgetInfo, id, undefined, createdTime, version);
return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails, return this.http.post<WidgetTypeDetails>('/api/widgetType', widgetTypeDetails,
defaultHttpOptionsFromConfig(config)).pipe( defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => { tap((savedWidgetType) => {

15
ui-ngx/src/app/core/interceptors/entity-conflict.interceptor.ts

@ -20,6 +20,7 @@ import {
HttpEvent, HttpEvent,
HttpHandler, HttpHandler,
HttpInterceptor, HttpInterceptor,
HttpParams,
HttpRequest, HttpRequest,
HttpStatusCode HttpStatusCode
} from '@angular/common/http'; } from '@angular/common/http';
@ -32,6 +33,8 @@ import {
import { HasId } from '@shared/models/base-data'; import { HasId } from '@shared/models/base-data';
import { HasVersion } from '@shared/models/entity.models'; import { HasVersion } from '@shared/models/entity.models';
import { getInterceptorConfig } from './interceptor.util'; import { getInterceptorConfig } from './interceptor.util';
import { isDefined } from '@core/utils';
import { InterceptorConfig } from '@core/interceptors/interceptor-config';
@Injectable() @Injectable()
export class EntityConflictInterceptor implements HttpInterceptor { export class EntityConflictInterceptor implements HttpInterceptor {
@ -67,8 +70,12 @@ export class EntityConflictInterceptor implements HttpInterceptor {
return this.openConflictDialog(request.body, error.error.message).pipe( return this.openConflictDialog(request.body, error.error.message).pipe(
switchMap(result => { switchMap(result => {
if (result) { if (isDefined(result)) {
return next.handle(this.updateRequestVersion(request)); if (result) {
return next.handle(this.updateRequestVersion(request));
}
(request.params as HttpParams & { interceptorConfig: InterceptorConfig }).interceptorConfig.ignoreErrors = true;
return throwError(() => error);
} }
return of(null); return of(null);
}) })
@ -82,7 +89,9 @@ export class EntityConflictInterceptor implements HttpInterceptor {
private openConflictDialog(entity: unknown & HasId & HasVersion, message: string): Observable<boolean> { private openConflictDialog(entity: unknown & HasId & HasVersion, message: string): Observable<boolean> {
const dialogRef = this.dialog.open(EntityConflictDialogComponent, { const dialogRef = this.dialog.open(EntityConflictDialogComponent, {
data: { message, entity } disableClose: true,
data: { message, entity },
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
}); });
return dialogRef.afterClosed(); return dialogRef.afterClosed();

222
ui-ngx/src/app/core/services/dashboard-utils.service.ts

@ -18,11 +18,16 @@ import { Injectable } from '@angular/core';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { import {
BreakpointId,
breakpointIdIconMap,
breakpointIdTranslationMap,
BreakpointInfo,
BreakpointLayoutInfo,
BreakpointSystemId,
Dashboard, Dashboard,
DashboardConfiguration, DashboardConfiguration,
DashboardLayout, DashboardLayout,
DashboardLayoutId, DashboardLayoutId,
DashboardLayoutInfo,
DashboardLayoutsInfo, DashboardLayoutsInfo,
DashboardState, DashboardState,
DashboardStateLayouts, DashboardStateLayouts,
@ -41,6 +46,7 @@ import {
Widget, Widget,
WidgetConfig, WidgetConfig,
WidgetConfigMode, WidgetConfigMode,
WidgetSize,
widgetType, widgetType,
WidgetTypeDescriptor WidgetTypeDescriptor
} from '@app/shared/models/widget.models'; } from '@app/shared/models/widget.models';
@ -51,14 +57,19 @@ import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'
import { AlarmSearchStatus } from '@shared/models/alarm.models'; import { AlarmSearchStatus } from '@shared/models/alarm.models';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models'; import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models';
import { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DashboardUtilsService { export class DashboardUtilsService {
private systemBreakpoints: {[key in BreakpointSystemId]?: BreakpointInfo};
constructor(private utils: UtilsService, constructor(private utils: UtilsService,
private timeService: TimeService) { private timeService: TimeService,
private translate: TranslateService) {
} }
public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard { public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard {
@ -523,6 +534,28 @@ export class DashboardUtilsService {
this.removeUnusedWidgets(dashboard); this.removeUnusedWidgets(dashboard);
} }
public isReferenceWidget(dashboard: Dashboard, widgetId: string): boolean {
const states = dashboard.configuration.states;
let foundWidgetRefs = 0;
for (const state of Object.values(states)) {
for (const layout of Object.values(state.layouts)) {
if (layout.widgets[widgetId]) {
foundWidgetRefs++;
}
if (layout.breakpoints) {
for (const breakpoint of Object.values(layout.breakpoints)) {
if (breakpoint.widgets[widgetId]) {
foundWidgetRefs++;
}
}
}
}
}
return foundWidgetRefs > 1;
}
public getRootStateId(states: {[id: string]: DashboardState }): string { public getRootStateId(states: {[id: string]: DashboardState }): string {
for (const stateId of Object.keys(states)) { for (const stateId of Object.keys(states)) {
const state = states[stateId]; const state = states[stateId];
@ -542,16 +575,14 @@ export class DashboardUtilsService {
for (const l of Object.keys(state.layouts)) { for (const l of Object.keys(state.layouts)) {
const layout: DashboardLayout = state.layouts[l]; const layout: DashboardLayout = state.layouts[l];
if (layout) { if (layout) {
result[l] = { result[l]= {
widgetIds: [], default: this.getBreakpointLayoutData(layout)
widgetLayouts: {}, };
gridSettings: {} if (layout.breakpoints) {
} as DashboardLayoutInfo; for (const breakpoint of Object.keys(layout.breakpoints)) {
for (const id of Object.keys(layout.widgets)) { result[l][breakpoint] = this.getBreakpointLayoutData(layout.breakpoints[breakpoint]);
result[l].widgetIds.push(id); }
} }
result[l].widgetLayouts = layout.widgets;
result[l].gridSettings = layout.gridSettings;
} }
} }
return result; return result;
@ -560,6 +591,20 @@ export class DashboardUtilsService {
} }
} }
private getBreakpointLayoutData(layout: DashboardLayout): BreakpointLayoutInfo {
const result: BreakpointLayoutInfo = {
widgetIds: [],
widgetLayouts: {},
gridSettings: {}
};
for (const id of Object.keys(layout.widgets)) {
result.widgetIds.push(id);
}
result.widgetLayouts = layout.widgets;
result.gridSettings = layout.gridSettings;
return result;
}
public getWidgetsArray(dashboard: Dashboard): Array<Widget> { public getWidgetsArray(dashboard: Dashboard): Array<Widget> {
const widgetsArray: Array<Widget> = []; const widgetsArray: Array<Widget> = [];
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
@ -586,11 +631,15 @@ export class DashboardUtilsService {
originalColumns?: number, originalColumns?: number,
originalSize?: {sizeX: number; sizeY: number}, originalSize?: {sizeX: number; sizeY: number},
row?: number, row?: number,
column?: number): void { column?: number,
breakpoint = 'default'): void {
const dashboardConfiguration = dashboard.configuration; const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states; const states = dashboardConfiguration.states;
const state = states[targetState]; const state = states[targetState];
const layout = state.layouts[targetLayout]; let layout = state.layouts[targetLayout];
if (breakpoint !== 'default' && layout.breakpoints?.[breakpoint]) {
layout = layout.breakpoints[breakpoint];
}
const layoutCount = Object.keys(state.layouts).length; const layoutCount = Object.keys(state.layouts).length;
if (!widget.id) { if (!widget.id) {
widget.id = this.utils.guid(); widget.id = this.utils.guid();
@ -648,11 +697,9 @@ export class DashboardUtilsService {
public removeWidgetFromLayout(dashboard: Dashboard, public removeWidgetFromLayout(dashboard: Dashboard,
targetState: string, targetState: string,
targetLayout: DashboardLayoutId, targetLayout: DashboardLayoutId,
widgetId: string) { widgetId: string,
const dashboardConfiguration = dashboard.configuration; breakpoint: BreakpointId) {
const states = dashboardConfiguration.states; const layout = this.getDashboardLayoutConfig(dashboard.configuration.states[targetState].layouts[targetLayout], breakpoint);
const state = states[targetState];
const layout = state.layouts[targetLayout];
delete layout.widgets[widgetId]; delete layout.widgets[widgetId];
this.removeUnusedWidgets(dashboard); this.removeUnusedWidgets(dashboard);
} }
@ -721,11 +768,19 @@ export class DashboardUtilsService {
for (const s of Object.keys(states)) { for (const s of Object.keys(states)) {
const state = states[s]; const state = states[s];
for (const l of Object.keys(state.layouts)) { for (const l of Object.keys(state.layouts)) {
const layout = state.layouts[l]; const layout: DashboardLayout = state.layouts[l];
if (layout.widgets[widgetId]) { if (layout.widgets[widgetId]) {
found = true; found = true;
break; break;
} }
if (layout.breakpoints) {
for (const breakpoint of Object.keys(layout.breakpoints)) {
if (layout.breakpoints[breakpoint].widgets[widgetId]) {
found = true;
break;
}
}
}
} }
} }
if (!found) { if (!found) {
@ -882,4 +937,133 @@ export class DashboardUtilsService {
} }
} }
replaceReferenceWithWidgetCopy(widget: Widget,
dashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,
breakpointId: BreakpointId,
isRemoveWidget: boolean): Widget {
const newWidget = deepClone(widget);
newWidget.id = this.utils.guid();
const originalColumns = this.getOriginalColumns(dashboard, targetState, targetLayout, breakpointId);
const originalSize = this.getOriginalSize(dashboard, targetState, targetLayout, widget, breakpointId);
const layout = this.getDashboardLayoutConfig(dashboard.configuration.states[targetState].layouts[targetLayout], breakpointId);
const widgetLayout = layout.widgets[widget.id];
const targetRow = widgetLayout.row;
const targetColumn = widgetLayout.col;
if (isRemoveWidget) {
this.removeWidgetFromLayout(dashboard, targetState, targetLayout, widget.id, breakpointId);
}
this.addWidgetToLayout(dashboard, targetState, targetLayout, newWidget, originalColumns, originalSize,
targetRow, targetColumn, breakpointId);
return newWidget;
}
getDashboardLayoutConfig(layout: DashboardLayout, breakpointId: BreakpointId): DashboardLayout {
if (breakpointId !== 'default' && layout.breakpoints) {
return layout.breakpoints[breakpointId];
}
return layout;
}
getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, breakpointId: BreakpointId): number {
let originalColumns = 24;
let gridSettings = null;
const state = dashboard.configuration.states[sourceState];
const layoutCount = Object.keys(state.layouts).length;
if (state) {
const layout = this.getDashboardLayoutConfig(state.layouts[sourceLayout], breakpointId);
if (layout) {
gridSettings = layout.gridSettings;
}
}
if (gridSettings && gridSettings.columns) {
originalColumns = gridSettings.columns;
}
originalColumns = originalColumns * layoutCount;
return originalColumns;
}
getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
widget: Widget, breakpointId: BreakpointId): WidgetSize {
const layout = this.getDashboardLayoutConfig(dashboard.configuration.states[sourceState].layouts[sourceLayout], breakpointId);
const widgetLayout = layout.widgets[widget.id];
return {
sizeX: widgetLayout.sizeX,
sizeY: widgetLayout.sizeY
};
}
private loadSystemBreakpoints() {
this.systemBreakpoints = {};
const dashboardMediaBreakpointIds: BreakpointSystemId[] = ['xs', 'sm', 'md', 'lg', 'xl'];
dashboardMediaBreakpointIds.forEach(breakpoint => {
const value = MediaBreakpoints[breakpoint];
const minWidth = value.match(/min-width:\s*(\d+)px/)?.[1];
const maxWidth = value.match(/max-width:\s*(\d+)px/)?.[1];
this.systemBreakpoints[breakpoint] = ({
id: breakpoint,
minWidth: minWidth ? Number(minWidth) : undefined,
maxWidth: maxWidth ? Number(maxWidth) : undefined,
value
});
});
}
getListBreakpoint(): BreakpointInfo[] {
if(!this.systemBreakpoints) {
this.loadSystemBreakpoints();
}
const breakpointsList = Object.values(this.systemBreakpoints);
breakpointsList.unshift({id: 'default'});
return breakpointsList;
}
getBreakpoints(): string[] {
if(!this.systemBreakpoints) {
this.loadSystemBreakpoints();
}
return Object.values(this.systemBreakpoints).map(item => item.value);
}
getBreakpointInfoByValue(breakpointValue: string): BreakpointInfo {
if(!this.systemBreakpoints) {
this.loadSystemBreakpoints();
}
return Object.values(this.systemBreakpoints).find(item => item.value === breakpointValue);
}
getBreakpointInfoById(breakpointId: BreakpointId): BreakpointInfo {
if(!this.systemBreakpoints) {
this.loadSystemBreakpoints();
}
return this.systemBreakpoints[breakpointId];
}
getBreakpointName(breakpointId: BreakpointId): string {
if (breakpointIdTranslationMap.has(breakpointId)) {
return this.translate.instant(breakpointIdTranslationMap.get(breakpointId));
}
return breakpointId;
}
getBreakpointIcon(breakpointId: BreakpointId): string {
if (breakpointIdIconMap.has(breakpointId)) {
return breakpointIdIconMap.get(breakpointId);
}
return 'desktop_windows';
}
getBreakpointSizeDescription(breakpointId: BreakpointId): string {
const currentData = this.getBreakpointInfoById(breakpointId);
const minStr = isDefined(currentData?.minWidth) ? `min ${currentData.minWidth}px` : '';
const maxStr = isDefined(currentData?.maxWidth) ? `max ${currentData.maxWidth}px` : '';
return minStr && maxStr ? `${minStr} - ${maxStr}` : `${minStr}${maxStr}`;
}
} }

81
ui-ngx/src/app/core/services/item-buffer.service.ts

@ -15,7 +15,7 @@
/// ///
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models'; import { BreakpointId, Dashboard, DashboardLayoutId } from '@app/shared/models/dashboard.models';
import { AliasesInfo, EntityAlias, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models'; import { AliasesInfo, EntityAlias, EntityAliases, EntityAliasInfo } from '@shared/models/alias.models';
import { import {
Datasource, Datasource,
@ -56,6 +56,7 @@ export interface WidgetReference {
widgetId: string; widgetId: string;
originalSize: WidgetSize; originalSize: WidgetSize;
originalColumns: number; originalColumns: number;
breakpoint: string;
} }
export interface RuleNodeConnection { export interface RuleNodeConnection {
@ -85,7 +86,8 @@ export class ItemBufferService {
private ruleChainService: RuleChainService, private ruleChainService: RuleChainService,
private utils: UtilsService) {} private utils: UtilsService) {}
public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetItem { public prepareWidgetItem(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
widget: Widget, breakpoint: BreakpointId): WidgetItem {
const aliasesInfo: AliasesInfo = { const aliasesInfo: AliasesInfo = {
datasourceAliases: {}, datasourceAliases: {},
targetDeviceAlias: null targetDeviceAlias: null
@ -93,8 +95,8 @@ export class ItemBufferService {
const filtersInfo: FiltersInfo = { const filtersInfo: FiltersInfo = {
datasourceFilters: {} datasourceFilters: {}
}; };
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
const datasources: Datasource[] = widget.type === widgetType.alarm ? [widget.config.alarmSource] : widget.config.datasources; const datasources: Datasource[] = widget.type === widgetType.alarm ? [widget.config.alarmSource] : widget.config.datasources;
if (widget.config && dashboard.configuration if (widget.config && dashboard.configuration
&& dashboard.configuration.entityAliases) { && dashboard.configuration.entityAliases) {
@ -146,13 +148,14 @@ export class ItemBufferService {
}; };
} }
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: BreakpointId) {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget); const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_ITEM, widgetItem); this.storeSet(WIDGET_ITEM, widgetItem);
} }
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void { public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget); widget: Widget, breakpoint: BreakpointId): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_REFERENCE, widgetReference); this.storeSet(WIDGET_REFERENCE, widgetReference);
} }
@ -160,11 +163,11 @@ export class ItemBufferService {
return this.storeHas(WIDGET_ITEM); return this.storeHas(WIDGET_ITEM);
} }
public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId): boolean { public canPasteWidgetReference(dashboard: Dashboard, state: string, layout: DashboardLayoutId, breakpoint: string): boolean {
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE); const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) { if (widgetReference) {
if (widgetReference.dashboardId === dashboard.id.id) { if (widgetReference.dashboardId === dashboard.id.id) {
if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout) if ((widgetReference.sourceState !== state || widgetReference.sourceLayout !== layout || widgetReference.breakpoint !== breakpoint)
&& dashboard.configuration.widgets[widgetReference.widgetId]) { && dashboard.configuration.widgets[widgetReference.widgetId]) {
return true; return true;
} }
@ -174,7 +177,9 @@ export class ItemBufferService {
} }
public pasteWidget(targetDashboard: Dashboard, targetState: string, public pasteWidget(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition, targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition,
onAliasesUpdateFunction: () => void, onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void): Observable<Widget> { onFiltersUpdateFunction: () => void): Observable<Widget> {
const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM); const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM);
@ -194,7 +199,7 @@ export class ItemBufferService {
return this.addWidgetToDashboard(targetDashboard, targetState, return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, aliasesInfo, filtersInfo, targetLayout, widget, aliasesInfo, filtersInfo,
onAliasesUpdateFunction, onFiltersUpdateFunction, onAliasesUpdateFunction, onFiltersUpdateFunction,
originalColumns, originalSize, targetRow, targetColumn).pipe( originalColumns, originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget) map(() => widget)
); );
} else { } else {
@ -202,8 +207,11 @@ export class ItemBufferService {
} }
} }
public pasteWidgetReference(targetDashboard: Dashboard, targetState: string, public pasteWidgetReference(targetDashboard: Dashboard,
targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> { targetState: string,
targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition): Observable<Widget> {
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE); const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) { if (widgetReference) {
const widget = targetDashboard.configuration.widgets[widgetReference.widgetId]; const widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
@ -219,7 +227,7 @@ export class ItemBufferService {
return this.addWidgetToDashboard(targetDashboard, targetState, return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, null, targetLayout, widget, null,
null, null, null, originalColumns, null, null, null, originalColumns,
originalSize, targetRow, targetColumn).pipe( originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget) map(() => widget)
); );
} else { } else {
@ -239,7 +247,8 @@ export class ItemBufferService {
originalColumns: number, originalColumns: number,
originalSize: WidgetSize, originalSize: WidgetSize,
row: number, row: number,
column: number): Observable<Dashboard> { column: number,
breakpoint = 'default'): Observable<Dashboard> {
let theDashboard: Dashboard; let theDashboard: Dashboard;
if (dashboard) { if (dashboard) {
theDashboard = dashboard; theDashboard = dashboard;
@ -270,7 +279,7 @@ export class ItemBufferService {
} }
} }
this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget, this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget,
originalColumns, originalSize, row, column); originalColumns, originalSize, row, column, breakpoint);
if (callAliasUpdateFunction) { if (callAliasUpdateFunction) {
onAliasesUpdateFunction(); onAliasesUpdateFunction();
} }
@ -387,35 +396,6 @@ export class ItemBufferService {
return ruleChainImport; return ruleChainImport;
} }
private getOriginalColumns(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId): number {
let originalColumns = 24;
let gridSettings = null;
const state = dashboard.configuration.states[sourceState];
const layoutCount = Object.keys(state.layouts).length;
if (state) {
const layout = state.layouts[sourceLayout];
if (layout) {
gridSettings = layout.gridSettings;
}
}
if (gridSettings &&
gridSettings.columns) {
originalColumns = gridSettings.columns;
}
originalColumns = originalColumns * layoutCount;
return originalColumns;
}
private getOriginalSize(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): WidgetSize {
const layout = dashboard.configuration.states[sourceState].layouts[sourceLayout];
const widgetLayout = layout.widgets[widget.id];
return {
sizeX: widgetLayout.sizeX,
sizeY: widgetLayout.sizeY
};
}
private prepareAliasInfo(entityAlias: EntityAlias): EntityAliasInfo { private prepareAliasInfo(entityAlias: EntityAlias): EntityAliasInfo {
return { return {
alias: entityAlias.alias, alias: entityAlias.alias,
@ -432,16 +412,17 @@ export class ItemBufferService {
} }
private prepareWidgetReference(dashboard: Dashboard, sourceState: string, private prepareWidgetReference(dashboard: Dashboard, sourceState: string,
sourceLayout: DashboardLayoutId, widget: Widget): WidgetReference { sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: BreakpointId): WidgetReference {
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout); const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget); const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
return { return {
dashboardId: dashboard.id.id, dashboardId: dashboard.id.id,
sourceState, sourceState,
sourceLayout, sourceLayout,
widgetId: widget.id, widgetId: widget.id,
originalSize, originalSize,
originalColumns originalColumns,
breakpoint
}; };
} }

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

@ -14,11 +14,14 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { HasUUID } from '@shared/models/id/has-uuid'; import { AuthState } from '@core/auth/auth.models';
import { Authority } from '@shared/models/authority.enum';
import { deepClone } from '@core/utils';
export declare type MenuSectionType = 'link' | 'toggle'; export declare type MenuSectionType = 'link' | 'toggle';
export interface MenuSection extends HasUUID{ export interface MenuSection {
id: MenuId | string;
name: string; name: string;
fullName?: string; fullName?: string;
type: MenuSectionType; type: MenuSectionType;
@ -26,17 +29,901 @@ export interface MenuSection extends HasUUID{
icon: string; icon: string;
pages?: Array<MenuSection>; pages?: Array<MenuSection>;
opened?: boolean; opened?: boolean;
disabled?: boolean;
rootOnly?: boolean; rootOnly?: boolean;
customTranslate?: boolean;
} }
export interface HomeSection { export interface MenuReference {
id: MenuId;
pages?: Array<MenuReference>;
}
export interface HomeSectionReference {
name: string; name: string;
places: Array<HomeSectionPlace>; places: Array<MenuId>;
} }
export interface HomeSectionPlace { export interface HomeSection {
name: string; name: string;
icon: string; places: Array<MenuSection>;
path: string; }
export enum MenuId {
home = 'home',
tenants = 'tenants',
tenant_profiles = 'tenant_profiles',
resources = 'resources',
widget_library = 'widget_library',
widget_types = 'widget_types',
widgets_bundles = 'widgets_bundles',
images = 'images',
scada_symbols = 'scada_symbols',
resources_library = 'resources_library',
notifications_center = 'notifications_center',
notification_inbox = 'notification_inbox',
notification_sent = 'notification_sent',
notification_recipients = 'notification_recipients',
notification_templates = 'notification_templates',
notification_rules = 'notification_rules',
settings = 'settings',
general = 'general',
mail_server = 'mail_server',
home_settings = 'home_settings',
notification_settings = 'notification_settings',
repository_settings = 'repository_settings',
auto_commit_settings = 'auto_commit_settings',
queues = 'queues',
mobile_app_settings = 'mobile_app_settings',
security_settings = 'security_settings',
security_settings_general = 'security_settings_general',
two_fa = '2fa',
oauth2 = 'oauth2',
audit_log = 'audit_log',
alarms = 'alarms',
dashboards = 'dashboards',
entities = 'entities',
devices = 'devices',
assets = 'assets',
entity_views = 'entity_views',
profiles = 'profiles',
device_profiles = 'device_profiles',
asset_profiles = 'asset_profiles',
customers = 'customers',
rule_chains = 'rule_chains',
edge_management = 'edge_management',
edges = 'edges',
edge_instances = 'edge_instances',
rulechain_templates = 'rulechain_templates',
features = 'features',
otaUpdates = 'otaUpdates',
version_control = 'version_control',
api_usage = 'api_usage'
} }
declare type MenuFilter = (authState: AuthState) => boolean;
export const menuSectionMap = new Map<MenuId, MenuSection>([
[
MenuId.home,
{
id: MenuId.home,
name: 'home.home',
type: 'link',
path: '/home',
icon: 'home'
}
],
[
MenuId.tenants,
{
id: MenuId.tenants,
name: 'tenant.tenants',
type: 'link',
path: '/tenants',
icon: 'supervisor_account'
}
],
[
MenuId.tenant_profiles,
{
id: MenuId.tenant_profiles,
name: 'tenant-profile.tenant-profiles',
type: 'link',
path: '/tenantProfiles',
icon: 'mdi:alpha-t-box'
}
],
[
MenuId.resources,
{
id: MenuId.resources,
name: 'admin.resources',
type: 'toggle',
path: '/resources',
icon: 'folder'
}
],
[
MenuId.widget_library,
{
id: MenuId.widget_library,
name: 'widget.widget-library',
type: 'link',
path: '/resources/widgets-library',
icon: 'now_widgets'
}
],
[
MenuId.widget_types,
{
id: MenuId.widget_types,
name: 'widget.widgets',
type: 'link',
path: '/resources/widgets-library/widget-types',
icon: 'now_widgets'
}
],
[
MenuId.widgets_bundles,
{
id: MenuId.widgets_bundles,
name: 'widgets-bundle.widgets-bundles',
type: 'link',
path: '/resources/widgets-library/widgets-bundles',
icon: 'now_widgets'
}
],
[
MenuId.images,
{
id: MenuId.images,
name: 'image.gallery',
type: 'link',
path: '/resources/images',
icon: 'filter'
}
],
[
MenuId.scada_symbols,
{
id: MenuId.scada_symbols,
name: 'scada.symbols',
type: 'link',
path: '/resources/scada-symbols',
icon: 'view_in_ar'
}
],
[
MenuId.resources_library,
{
id: MenuId.resources_library,
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split'
}
],
[
MenuId.notifications_center,
{
id: MenuId.notifications_center,
name: 'notification.notification-center',
type: 'link',
path: '/notification',
icon: 'mdi:message-badge'
}
],
[
MenuId.notification_inbox,
{
id: MenuId.notification_inbox,
name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link',
path: '/notification/inbox',
icon: 'inbox'
}
],
[
MenuId.notification_sent,
{
id: MenuId.notification_sent,
name: 'notification.sent',
fullName: 'notification.notification-sent',
type: 'link',
path: '/notification/sent',
icon: 'outbox'
}
],
[
MenuId.notification_recipients,
{
id: MenuId.notification_recipients,
name: 'notification.recipients',
fullName: 'notification.notification-recipients',
type: 'link',
path: '/notification/recipients',
icon: 'contacts'
}
],
[
MenuId.notification_templates,
{
id: MenuId.notification_templates,
name: 'notification.templates',
fullName: 'notification.notification-templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw'
}
],
[
MenuId.notification_rules,
{
id: MenuId.notification_rules,
name: 'notification.rules',
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog'
}
],
[
MenuId.settings,
{
id: MenuId.settings,
name: 'admin.settings',
type: 'link',
path: '/settings',
icon: 'settings'
}
],
[
MenuId.general,
{
id: MenuId.general,
name: 'admin.general',
fullName: 'admin.general-settings',
type: 'link',
path: '/settings/general',
icon: 'settings_applications'
}
],
[
MenuId.mail_server,
{
id: MenuId.mail_server,
name: 'admin.outgoing-mail',
type: 'link',
path: '/settings/outgoing-mail',
icon: 'mail'
}
],
[
MenuId.home_settings,
{
id: MenuId.home_settings,
name: 'admin.home',
fullName: 'admin.home-settings',
type: 'link',
path: '/settings/home',
icon: 'settings_applications'
}
],
[
MenuId.notification_settings,
{
id: MenuId.notification_settings,
name: 'admin.notifications',
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge'
}
],
[
MenuId.repository_settings,
{
id: MenuId.repository_settings,
name: 'admin.repository',
fullName: 'admin.repository-settings',
type: 'link',
path: '/settings/repository',
icon: 'manage_history'
}
],
[
MenuId.auto_commit_settings,
{
id: MenuId.auto_commit_settings,
name: 'admin.auto-commit',
fullName: 'admin.auto-commit-settings',
type: 'link',
path: '/settings/auto-commit',
icon: 'settings_backup_restore'
}
],
[
MenuId.queues,
{
id: MenuId.queues,
name: 'admin.queues',
type: 'link',
path: '/settings/queues',
icon: 'swap_calls'
}
],
[
MenuId.mobile_app_settings,
{
id: MenuId.mobile_app_settings,
name: 'admin.mobile-app.mobile-app',
fullName: 'admin.mobile-app.mobile-app',
type: 'link',
path: '/settings/mobile-app',
icon: 'smartphone'
}
],
[
MenuId.security_settings,
{
id: MenuId.security_settings,
name: 'security.security',
type: 'toggle',
path: '/security-settings',
icon: 'security'
}
],
[
MenuId.security_settings_general,
{
id: MenuId.security_settings_general,
name: 'admin.general',
fullName: 'security.general-settings',
type: 'link',
path: '/security-settings/general',
icon: 'settings_applications'
}
],
[
MenuId.two_fa,
{
id: MenuId.two_fa,
name: 'admin.2fa.2fa',
type: 'link',
path: '/security-settings/2fa',
icon: 'mdi:two-factor-authentication'
}
],
[
MenuId.oauth2,
{
id: MenuId.oauth2,
name: 'admin.oauth2.oauth2',
type: 'link',
path: '/security-settings/oauth2',
icon: 'mdi:shield-account'
}
],
[
MenuId.audit_log,
{
id: MenuId.audit_log,
name: 'audit-log.audit-logs',
type: 'link',
path: '/security-settings/auditLogs',
icon: 'track_changes'
}
],
[
MenuId.alarms,
{
id: MenuId.alarms,
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline'
}
],
[
MenuId.dashboards,
{
id: MenuId.dashboards,
name: 'dashboard.dashboards',
type: 'link',
path: '/dashboards',
icon: 'dashboards'
}
],
[
MenuId.entities,
{
id: MenuId.entities,
name: 'entity.entities',
type: 'toggle',
path: '/entities',
icon: 'category'
}
],
[
MenuId.devices,
{
id: MenuId.devices,
name: 'device.devices',
type: 'link',
path: '/entities/devices',
icon: 'devices_other'
}
],
[
MenuId.assets,
{
id: MenuId.assets,
name: 'asset.assets',
type: 'link',
path: '/entities/assets',
icon: 'domain'
}
],
[
MenuId.entity_views,
{
id: MenuId.entity_views,
name: 'entity-view.entity-views',
type: 'link',
path: '/entities/entityViews',
icon: 'view_quilt'
}
],
[
MenuId.profiles,
{
id: MenuId.profiles,
name: 'profiles.profiles',
type: 'toggle',
path: '/profiles',
icon: 'badge'
}
],
[
MenuId.device_profiles,
{
id: MenuId.device_profiles,
name: 'device-profile.device-profiles',
type: 'link',
path: '/profiles/deviceProfiles',
icon: 'mdi:alpha-d-box'
}
],
[
MenuId.asset_profiles,
{
id: MenuId.asset_profiles,
name: 'asset-profile.asset-profiles',
type: 'link',
path: '/profiles/assetProfiles',
icon: 'mdi:alpha-a-box'
}
],
[
MenuId.customers,
{
id: MenuId.customers,
name: 'customer.customers',
type: 'link',
path: '/customers',
icon: 'supervisor_account'
}
],
[
MenuId.rule_chains,
{
id: MenuId.rule_chains,
name: 'rulechain.rulechains',
type: 'link',
path: '/ruleChains',
icon: 'settings_ethernet'
}
],
[
MenuId.edge_management,
{
id: MenuId.edge_management,
name: 'edge.management',
type: 'toggle',
path: '/edgeManagement',
icon: 'settings_input_antenna'
}
],
[
MenuId.edges,
{
id: MenuId.edges,
name: 'edge.instances',
fullName: 'edge.edge-instances',
type: 'link',
path: '/edgeManagement/instances',
icon: 'router'
}
],
[
MenuId.edge_instances,
{
id: MenuId.edge_instances,
name: 'edge.edge-instances',
fullName: 'edge.edge-instances',
type: 'link',
path: '/edgeManagement/instances',
icon: 'router'
}
],
[
MenuId.rulechain_templates,
{
id: MenuId.rulechain_templates,
name: 'edge.rulechain-templates',
fullName: 'edge.edge-rulechain-templates',
type: 'link',
path: '/edgeManagement/ruleChains',
icon: 'settings_ethernet'
}
],
[
MenuId.features,
{
id: MenuId.features,
name: 'feature.advanced-features',
type: 'toggle',
path: '/features',
icon: 'construction'
}
],
[
MenuId.otaUpdates,
{
id: MenuId.otaUpdates,
name: 'ota-update.ota-updates',
type: 'link',
path: '/features/otaUpdates',
icon: 'memory'
}
],
[
MenuId.version_control,
{
id: MenuId.version_control,
name: 'version-control.version-control',
type: 'link',
path: '/features/vc',
icon: 'history'
}
],
[
MenuId.api_usage,
{
id: MenuId.api_usage,
name: 'api-usage.api-usage',
type: 'link',
path: '/usage',
icon: 'insert_chart'
}
]
]);
const menuFilters = new Map<MenuId, MenuFilter>([
[
MenuId.edges, (authState) => authState.edgesSupportEnabled
],
[
MenuId.edge_management, (authState) => authState.edgesSupportEnabled
],
[
MenuId.rulechain_templates, (authState) => authState.edgesSupportEnabled
]
]);
const defaultUserMenuMap = new Map<Authority, MenuReference[]>([
[
Authority.SYS_ADMIN,
[
{id: MenuId.home},
{id: MenuId.tenants},
{id: MenuId.tenant_profiles},
{
id: MenuId.resources,
pages: [
{
id: MenuId.widget_library,
pages: [
{id: MenuId.widget_types},
{id: MenuId.widgets_bundles}
]
},
{id: MenuId.images},
{id: MenuId.scada_symbols},
{id: MenuId.resources_library}
]
},
{
id: MenuId.notifications_center,
pages: [
{id: MenuId.notification_inbox},
{id: MenuId.notification_sent},
{id: MenuId.notification_recipients},
{id: MenuId.notification_templates},
{id: MenuId.notification_rules}
]
},
{
id: MenuId.settings,
pages: [
{id: MenuId.general},
{id: MenuId.mail_server},
{id: MenuId.notification_settings},
{id: MenuId.queues},
{id: MenuId.mobile_app_settings}
]
},
{
id: MenuId.security_settings,
pages: [
{id: MenuId.security_settings_general},
{id: MenuId.two_fa},
{id: MenuId.oauth2}
]
}
]
],
[
Authority.TENANT_ADMIN,
[
{id: MenuId.home},
{id: MenuId.alarms},
{id: MenuId.dashboards},
{
id: MenuId.entities,
pages: [
{id: MenuId.devices},
{id: MenuId.assets},
{id: MenuId.entity_views}
]
},
{
id: MenuId.profiles,
pages: [
{id: MenuId.device_profiles},
{id: MenuId.asset_profiles}
]
},
{id: MenuId.customers},
{id: MenuId.rule_chains},
{
id: MenuId.edge_management,
pages: [
{id: MenuId.edges},
{id: MenuId.rulechain_templates}
]
},
{
id: MenuId.features,
pages: [
{id: MenuId.otaUpdates},
{id: MenuId.version_control}
]
},
{
id: MenuId.resources,
pages: [
{
id: MenuId.widget_library,
pages: [
{id: MenuId.widget_types},
{id: MenuId.widgets_bundles}
]
},
{id: MenuId.images},
{id: MenuId.scada_symbols},
{id: MenuId.resources_library}
]
},
{
id: MenuId.notifications_center,
pages: [
{id: MenuId.notification_inbox},
{id: MenuId.notification_sent},
{id: MenuId.notification_recipients},
{id: MenuId.notification_templates},
{id: MenuId.notification_rules}
]
},
{id: MenuId.api_usage},
{
id: MenuId.settings,
pages: [
{id: MenuId.home_settings},
{id: MenuId.notification_settings},
{id: MenuId.repository_settings},
{id: MenuId.auto_commit_settings}
]
},
{
id: MenuId.security_settings,
pages: [
{id: MenuId.audit_log}
]
}
]
],
[
Authority.CUSTOMER_USER,
[
{id: MenuId.home},
{id: MenuId.alarms},
{id: MenuId.dashboards},
{
id: MenuId.entities,
pages: [
{id: MenuId.devices},
{id: MenuId.assets},
{id: MenuId.entity_views}
]
},
{id: MenuId.edge_instances},
{
id: MenuId.notifications_center,
pages: [
{id: MenuId.notification_inbox}
]
}
]
]
]);
const defaultHomeSectionMap = new Map<Authority, HomeSectionReference[]>([
[
Authority.SYS_ADMIN,
[
{
name: 'tenant.management',
places: [MenuId.tenants, MenuId.tenant_profiles]
},
{
name: 'widget.management',
places: [MenuId.widget_library]
},
{
name: 'admin.system-settings',
places: [MenuId.general, MenuId.mail_server,
MenuId.notification_settings, MenuId.security_settings, MenuId.oauth2, MenuId.two_fa, MenuId.resources_library, MenuId.queues]
}
]
],
[
Authority.TENANT_ADMIN,
[
{
name: 'rulechain.management',
places: [MenuId.rule_chains]
},
{
name: 'customer.management',
places: [MenuId.customers]
},
{
name: 'asset.management',
places: [MenuId.assets, MenuId.asset_profiles]
},
{
name: 'device.management',
places: [MenuId.devices, MenuId.device_profiles, MenuId.otaUpdates]
},
{
name: 'entity-view.management',
places: [MenuId.entity_views]
},
{
name: 'edge.management',
places: [MenuId.edges, MenuId.rulechain_templates]
},
{
name: 'dashboard.management',
places: [MenuId.widget_library, MenuId.dashboards]
},
{
name: 'version-control.management',
places: [MenuId.version_control]
},
{
name: 'audit-log.audit',
places: [MenuId.audit_log, MenuId.api_usage]
},
{
name: 'admin.system-settings',
places: [MenuId.home_settings, MenuId.resources_library, MenuId.repository_settings, MenuId.auto_commit_settings]
}
]
],
[
Authority.CUSTOMER_USER,
[
{
name: 'asset.view-assets',
places: [MenuId.assets]
},
{
name: 'device.view-devices',
places: [MenuId.devices]
},
{
name: 'entity-view.management',
places: [MenuId.entity_views]
},
{
name: 'edge.management',
places: [MenuId.edge_instances]
},
{
name: 'dashboard.view-dashboards',
places: [MenuId.dashboards]
}
]
]
]);
export const buildUserMenu = (authState: AuthState): Array<MenuSection> => {
const references = defaultUserMenuMap.get(authState.authUser.authority);
return (references || []).map(ref => referenceToMenuSection(authState, ref)).filter(section => !!section);
};
export const buildUserHome = (authState: AuthState, availableMenuSections: MenuSection[]): Array<HomeSection> => {
const references = defaultHomeSectionMap.get(authState.authUser.authority);
return (references || []).map(ref =>
homeReferenceToHomeSection(availableMenuSections, ref)).filter(section => !!section);
};
const referenceToMenuSection = (authState: AuthState, reference: MenuReference): MenuSection | undefined => {
if (filterMenuReference(authState, reference)) {
const section = menuSectionMap.get(reference.id);
if (section) {
const result = deepClone(section);
if (reference.pages?.length) {
result.pages = reference.pages.map(page =>
referenceToMenuSection(authState, page)).filter(page => !!page);
}
return result;
} else {
return undefined;
}
} else {
return undefined;
}
};
const filterMenuReference = (authState: AuthState, reference: MenuReference): boolean => {
const filter = menuFilters.get(reference.id);
if (filter) {
if (filter(authState)) {
if (reference.pages?.length) {
if (reference.pages.every(page => !filterMenuReference(authState, page))) {
return false;
}
}
return true;
}
return false;
} else {
return true;
}
};
const homeReferenceToHomeSection = (availableMenuSections: MenuSection[], reference: HomeSectionReference): HomeSection | undefined => {
const places = reference.places.map(id => availableMenuSections.find(m => m.id === id)).filter(p => !!p);
if (places.length) {
return {
name: reference.name,
places
};
} else {
return undefined;
}
};

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

@ -19,9 +19,8 @@ import { select, Store } from '@ngrx/store';
import { AppState } from '../core.state'; import { AppState } from '../core.state';
import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors'; import { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors';
import { filter, map, take } from 'rxjs/operators'; import { filter, map, take } from 'rxjs/operators';
import { HomeSection, MenuSection } from '@core/services/menu.models'; import { buildUserHome, buildUserMenu, HomeSection, MenuId, MenuSection } from '@core/services/menu.models';
import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { Observable, ReplaySubject, Subject } from 'rxjs';
import { Authority } from '@shared/models/authority.enum';
import { AuthState } from '@core/auth/auth.models'; import { AuthState } from '@core/auth/auth.models';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
@ -30,10 +29,11 @@ import { NavigationEnd, Router } from '@angular/router';
}) })
export class MenuService { export class MenuService {
currentMenuSections: Array<MenuSection>; private currentMenuSections: Array<MenuSection>;
menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]); private menuSections$: Subject<Array<MenuSection>> = new ReplaySubject<Array<MenuSection>>(1);
homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]); private homeSections$: Subject<Array<HomeSection>> = new ReplaySubject<Array<HomeSection>>(1);
availableMenuLinks$ = this.menuSections$.pipe( private availableMenuSections$: Subject<Array<MenuSection>> = new ReplaySubject<Array<MenuSection>>(1);
private availableMenuLinks$ = this.menuSections$.pipe(
map((items) => this.allMenuLinks(items)) map((items) => this.allMenuLinks(items))
); );
@ -57,23 +57,12 @@ export class MenuService {
this.store.pipe(select(selectAuth), take(1)).subscribe( this.store.pipe(select(selectAuth), take(1)).subscribe(
(authState: AuthState) => { (authState: AuthState) => {
if (authState.authUser) { if (authState.authUser) {
let homeSections: Array<HomeSection>; this.currentMenuSections = buildUserMenu(authState);
switch (authState.authUser.authority) {
case Authority.SYS_ADMIN:
this.currentMenuSections = this.buildSysAdminMenu();
homeSections = this.buildSysAdminHome();
break;
case Authority.TENANT_ADMIN:
this.currentMenuSections = this.buildTenantAdminMenu(authState);
homeSections = this.buildTenantAdminHome(authState);
break;
case Authority.CUSTOMER_USER:
this.currentMenuSections = this.buildCustomerUserMenu(authState);
homeSections = this.buildCustomerUserHome(authState);
break;
}
this.updateOpenedMenuSections(); this.updateOpenedMenuSections();
this.menuSections$.next(this.currentMenuSections); this.menuSections$.next(this.currentMenuSections);
const availableMenuSections = this.allMenuSections(this.currentMenuSections);
this.availableMenuSections$.next(availableMenuSections);
const homeSections = buildUserHome(authState, availableMenuSections);
this.homeSections$.next(homeSections); this.homeSections$.next(homeSections);
} }
} }
@ -89,916 +78,6 @@ export class MenuService {
); );
} }
private buildSysAdminMenu(): Array<MenuSection> {
const sections: Array<MenuSection> = [];
sections.push(
{
id: 'home',
name: 'home.home',
type: 'link',
path: '/home',
icon: 'home'
},
{
id: 'tenants',
name: 'tenant.tenants',
type: 'link',
path: '/tenants',
icon: 'supervisor_account'
},
{
id: 'tenant_profiles',
name: 'tenant-profile.tenant-profiles',
type: 'link',
path: '/tenantProfiles',
icon: 'mdi:alpha-t-box'
},
{
id: 'resources',
name: 'admin.resources',
type: 'toggle',
path: '/resources',
icon: 'folder',
pages: [
{
id: 'widget_library',
name: 'widget.widget-library',
type: 'link',
path: '/resources/widgets-library',
icon: 'now_widgets',
pages: [
{
id: 'widget_types',
name: 'widget.widgets',
type: 'link',
path: '/resources/widgets-library/widget-types',
icon: 'now_widgets'
},
{
id: 'widgets_bundles',
name: 'widgets-bundle.widgets-bundles',
type: 'link',
path: '/resources/widgets-library/widgets-bundles',
icon: 'now_widgets'
}
]
},
{
id: 'images',
name: 'image.gallery',
type: 'link',
path: '/resources/images',
icon: 'filter'
},
{
id: 'scada_symbols',
name: 'scada.symbols',
type: 'link',
path: '/resources/scada-symbols',
icon: 'view_in_ar'
},
{
id: 'resources_library',
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split'
}
]
},
{
id: 'notifications_center',
name: 'notification.notification-center',
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
pages: [
{
id: 'notification_inbox',
name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link',
path: '/notification/inbox',
icon: 'inbox'
},
{
id: 'notification_sent',
name: 'notification.sent',
fullName: 'notification.notification-sent',
type: 'link',
path: '/notification/sent',
icon: 'outbox'
},
{
id: 'notification_recipients',
name: 'notification.recipients',
fullName: 'notification.notification-recipients',
type: 'link',
path: '/notification/recipients',
icon: 'contacts'
},
{
id: 'notification_templates',
name: 'notification.templates',
fullName: 'notification.notification-templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw'
},
{
id: 'notification_rules',
name: 'notification.rules',
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog'
}
]
},
{
id: 'settings',
name: 'admin.settings',
type: 'link',
path: '/settings',
icon: 'settings',
pages: [
{
id: 'general',
name: 'admin.general',
fullName: 'admin.general-settings',
type: 'link',
path: '/settings/general',
icon: 'settings_applications'
},
{
id: 'mail_server',
name: 'admin.outgoing-mail',
type: 'link',
path: '/settings/outgoing-mail',
icon: 'mail'
},
{
id: 'notification_settings',
name: 'admin.notifications',
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge'
},
{
id: 'queues',
name: 'admin.queues',
type: 'link',
path: '/settings/queues',
icon: 'swap_calls'
},
{
id: 'mobile_app_settings',
name: 'admin.mobile-app.mobile-app',
fullName: 'admin.mobile-app.mobile-app',
type: 'link',
path: '/settings/mobile-app',
icon: 'smartphone'
}
]
},
{
id: 'security_settings',
name: 'security.security',
type: 'toggle',
path: '/security-settings',
icon: 'security',
pages: [
{
id: 'security_settings_general',
name: 'admin.general',
fullName: 'security.general-settings',
type: 'link',
path: '/security-settings/general',
icon: 'settings_applications'
},
{
id: '2fa',
name: 'admin.2fa.2fa',
type: 'link',
path: '/security-settings/2fa',
icon: 'mdi:two-factor-authentication'
},
{
id: 'oauth2',
name: 'admin.oauth2.oauth2',
type: 'link',
path: '/security-settings/oauth2',
icon: 'mdi:shield-account'
}
]
}
);
return sections;
}
private buildSysAdminHome(): Array<HomeSection> {
const homeSections: Array<HomeSection> = [];
homeSections.push(
{
name: 'tenant.management',
places: [
{
name: 'tenant.tenants',
icon: 'supervisor_account',
path: '/tenants'
},
{
name: 'tenant-profile.tenant-profiles',
icon: 'mdi:alpha-t-box',
path: '/tenantProfiles'
},
]
},
{
name: 'widget.management',
places: [
{
name: 'widget.widget-library',
icon: 'now_widgets',
path: '/resources/widgets-library',
}
]
},
{
name: 'admin.system-settings',
places: [
{
name: 'admin.general',
icon: 'settings_applications',
path: '/settings/general'
},
{
name: 'admin.outgoing-mail',
icon: 'mail',
path: '/settings/outgoing-mail'
},
{
name: 'admin.sms-provider',
icon: 'sms',
path: '/settings/sms-provider'
},
{
name: 'admin.security-settings',
icon: 'security',
path: '/settings/security-settings'
},
{
name: 'admin.oauth2.oauth2',
icon: 'security',
path: '/settings/oauth2'
},
{
name: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication',
path: '/settings/2fa'
},
{
name: 'resource.resources-library',
icon: 'folder',
path: '/settings/resources-library'
},
{
name: 'admin.queues',
icon: 'swap_calls',
path: '/settings/queues'
},
]
}
);
return homeSections;
}
private buildTenantAdminMenu(authState: AuthState): Array<MenuSection> {
const sections: Array<MenuSection> = [];
sections.push(
{
id: 'home',
name: 'home.home',
type: 'link',
path: '/home',
icon: 'home'
},
{
id: 'alarms',
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline'
},
{
id: 'dashboards',
name: 'dashboard.dashboards',
type: 'link',
path: '/dashboards',
icon: 'dashboards'
},
{
id: 'entities',
name: 'entity.entities',
type: 'toggle',
path: '/entities',
icon: 'category',
pages: [
{
id: 'devices',
name: 'device.devices',
type: 'link',
path: '/entities/devices',
icon: 'devices_other'
},
{
id: 'assets',
name: 'asset.assets',
type: 'link',
path: '/entities/assets',
icon: 'domain'
},
{
id: 'entity_views',
name: 'entity-view.entity-views',
type: 'link',
path: '/entities/entityViews',
icon: 'view_quilt'
}
]
},
{
id: 'profiles',
name: 'profiles.profiles',
type: 'toggle',
path: '/profiles',
icon: 'badge',
pages: [
{
id: 'device_profiles',
name: 'device-profile.device-profiles',
type: 'link',
path: '/profiles/deviceProfiles',
icon: 'mdi:alpha-d-box'
},
{
id: 'asset_profiles',
name: 'asset-profile.asset-profiles',
type: 'link',
path: '/profiles/assetProfiles',
icon: 'mdi:alpha-a-box'
}
]
},
{
id: 'customers',
name: 'customer.customers',
type: 'link',
path: '/customers',
icon: 'supervisor_account'
},
{
id: 'rule_chains',
name: 'rulechain.rulechains',
type: 'link',
path: '/ruleChains',
icon: 'settings_ethernet'
}
);
if (authState.edgesSupportEnabled) {
sections.push(
{
id: 'edge_management',
name: 'edge.management',
type: 'toggle',
path: '/edgeManagement',
icon: 'settings_input_antenna',
pages: [
{
id: 'edges',
name: 'edge.instances',
fullName: 'edge.edge-instances',
type: 'link',
path: '/edgeManagement/instances',
icon: 'router'
},
{
id: 'rulechain_templates',
name: 'edge.rulechain-templates',
fullName: 'edge.edge-rulechain-templates',
type: 'link',
path: '/edgeManagement/ruleChains',
icon: 'settings_ethernet'
}
]
}
);
}
sections.push(
{
id: 'features',
name: 'feature.advanced-features',
type: 'toggle',
path: '/features',
icon: 'construction',
pages: [
{
id: 'otaUpdates',
name: 'ota-update.ota-updates',
type: 'link',
path: '/features/otaUpdates',
icon: 'memory'
},
{
id: 'version_control',
name: 'version-control.version-control',
type: 'link',
path: '/features/vc',
icon: 'history'
}
]
},
{
id: 'resources',
name: 'admin.resources',
type: 'toggle',
path: '/resources',
icon: 'folder',
pages: [
{
id: 'widget_library',
name: 'widget.widget-library',
type: 'link',
path: '/resources/widgets-library',
icon: 'now_widgets',
pages: [
{
id: 'widget_types',
name: 'widget.widgets',
type: 'link',
path: '/resources/widgets-library/widget-types',
icon: 'now_widgets'
},
{
id: 'widgets_bundles',
name: 'widgets-bundle.widgets-bundles',
type: 'link',
path: '/resources/widgets-library/widgets-bundles',
icon: 'now_widgets'
}
]
},
{
id: 'images',
name: 'image.gallery',
type: 'link',
path: '/resources/images',
icon: 'filter'
},
{
id: 'scada_symbols',
name: 'scada.symbols',
type: 'link',
path: '/resources/scada-symbols',
icon: 'view_in_ar'
},
{
id: 'resources_library',
name: 'resource.resources-library',
type: 'link',
path: '/resources/resources-library',
icon: 'mdi:rhombus-split'
}
]
},
{
id: 'notifications_center',
name: 'notification.notification-center',
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
pages: [
{
id: 'notification_inbox',
name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link',
path: '/notification/inbox',
icon: 'inbox'
},
{
id: 'notification_sent',
name: 'notification.sent',
fullName: 'notification.notification-sent',
type: 'link',
path: '/notification/sent',
icon: 'outbox'
},
{
id: 'notification_recipients',
name: 'notification.recipients',
fullName: 'notification.notification-recipients',
type: 'link',
path: '/notification/recipients',
icon: 'contacts'
},
{
id: 'notification_templates',
name: 'notification.templates',
fullName: 'notification.notification-templates',
type: 'link',
path: '/notification/templates',
icon: 'mdi:message-draw'
},
{
id: 'notification_rules',
name: 'notification.rules',
fullName: 'notification.notification-rules',
type: 'link',
path: '/notification/rules',
icon: 'mdi:message-cog'
}
]
},
{
id: 'api_usage',
name: 'api-usage.api-usage',
type: 'link',
path: '/usage',
icon: 'insert_chart'
},
{
id: 'settings',
name: 'admin.settings',
type: 'link',
path: '/settings',
icon: 'settings',
pages: [
{
id: 'home_settings',
name: 'admin.home',
fullName: 'admin.home-settings',
type: 'link',
path: '/settings/home',
icon: 'settings_applications'
},
{
id: 'notification_settings',
name: 'admin.notifications',
fullName: 'admin.notifications-settings',
type: 'link',
path: '/settings/notifications',
icon: 'mdi:message-badge'
},
{
id: 'repository_settings',
name: 'admin.repository',
fullName: 'admin.repository-settings',
type: 'link',
path: '/settings/repository',
icon: 'manage_history'
},
{
id: 'auto_commit_settings',
name: 'admin.auto-commit',
fullName: 'admin.auto-commit-settings',
type: 'link',
path: '/settings/auto-commit',
icon: 'settings_backup_restore'
}
]
},
{
id: 'security_settings',
name: 'security.security',
type: 'toggle',
path: '/security-settings',
icon: 'security',
pages: [
{
id: 'audit_log',
name: 'audit-log.audit-logs',
type: 'link',
path: '/security-settings/auditLogs',
icon: 'track_changes'
}
]
}
);
return sections;
}
private buildTenantAdminHome(authState: AuthState): Array<HomeSection> {
const homeSections: Array<HomeSection> = [];
homeSections.push(
{
name: 'rulechain.management',
places: [
{
name: 'rulechain.rulechains',
icon: 'settings_ethernet',
path: '/ruleChains'
}
]
},
{
name: 'customer.management',
places: [
{
name: 'customer.customers',
icon: 'supervisor_account',
path: '/customers'
}
]
},
{
name: 'asset.management',
places: [
{
name: 'asset.assets',
icon: 'domain',
path: '/assets'
},
{
name: 'asset-profile.asset-profiles',
icon: 'mdi:alpha-a-box',
path: '/profiles/assetProfiles'
}
]
},
{
name: 'device.management',
places: [
{
name: 'device.devices',
icon: 'devices_other',
path: '/devices'
},
{
name: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box',
path: '/profiles/deviceProfiles'
},
{
name: 'ota-update.ota-updates',
icon: 'memory',
path: '/otaUpdates'
}
]
},
{
name: 'entity-view.management',
places: [
{
name: 'entity-view.entity-views',
icon: 'view_quilt',
path: '/entityViews'
}
]
}
);
if (authState.edgesSupportEnabled) {
homeSections.push(
{
name: 'edge.management',
places: [
{
name: 'edge.edge-instances',
icon: 'router',
path: '/edgeInstances'
},
{
name: 'edge.rulechain-templates',
icon: 'settings_ethernet',
path: '/edgeManagement/ruleChains'
}
]
}
);
}
homeSections.push(
{
name: 'dashboard.management',
places: [
{
name: 'widget.widget-library',
icon: 'now_widgets',
path: '/widgets-bundles'
},
{
name: 'dashboard.dashboards',
icon: 'dashboard',
path: '/dashboards'
}
]
},
{
name: 'version-control.management',
places: [
{
name: 'version-control.version-control',
icon: 'history',
path: '/vc'
}
]
},
{
name: 'audit-log.audit',
places: [
{
name: 'audit-log.audit-logs',
icon: 'track_changes',
path: '/auditLogs'
},
{
name: 'api-usage.api-usage',
icon: 'insert_chart',
path: '/usage'
}
]
},
{
name: 'admin.system-settings',
places: [
{
name: 'admin.home-settings',
icon: 'settings_applications',
path: '/settings/home'
},
{
name: 'resource.resources-library',
icon: 'folder',
path: '/settings/resources-library'
},
{
name: 'admin.repository-settings',
icon: 'manage_history',
path: '/settings/repository',
},
{
name: 'admin.auto-commit-settings',
icon: 'settings_backup_restore',
path: '/settings/auto-commit'
}
]
}
);
return homeSections;
}
private buildCustomerUserMenu(authState: AuthState): Array<MenuSection> {
const sections: Array<MenuSection> = [];
sections.push(
{
id: 'home',
name: 'home.home',
type: 'link',
path: '/home',
icon: 'home'
},
{
id: 'alarms',
name: 'alarm.alarms',
type: 'link',
path: '/alarms',
icon: 'mdi:alert-outline'
},
{
id: 'dashboards',
name: 'dashboard.dashboards',
type: 'link',
path: '/dashboards',
icon: 'dashboards'
},
{
id: 'entities',
name: 'entity.entities',
type: 'toggle',
path: '/entities',
icon: 'category',
pages: [
{
id: 'devices',
name: 'device.devices',
type: 'link',
path: '/entities/devices',
icon: 'devices_other'
},
{
id: 'assets',
name: 'asset.assets',
type: 'link',
path: '/entities/assets',
icon: 'domain'
},
{
id: 'entity_views',
name: 'entity-view.entity-views',
type: 'link',
path: '/entities/entityViews',
icon: 'view_quilt'
}
]
}
);
if (authState.edgesSupportEnabled) {
sections.push(
{
id: 'edges',
name: 'edge.edge-instances',
fullName: 'edge.edge-instances',
type: 'link',
path: '/edgeManagement/instances',
icon: 'router'
}
);
}
sections.push(
{
id: 'notifications_center',
name: 'notification.notification-center',
type: 'link',
path: '/notification',
icon: 'mdi:message-badge',
pages: [
{
id: 'notification_inbox',
name: 'notification.inbox',
fullName: 'notification.notification-inbox',
type: 'link',
path: '/notification/inbox',
icon: 'inbox'
}
]
}
);
return sections;
}
private buildCustomerUserHome(authState: AuthState): Array<HomeSection> {
const homeSections: Array<HomeSection> = [];
homeSections.push(
{
name: 'asset.view-assets',
places: [
{
name: 'asset.assets',
icon: 'domain',
path: '/assets'
}
]
},
{
name: 'device.view-devices',
places: [
{
name: 'device.devices',
icon: 'devices_other',
path: '/devices'
}
]
},
{
name: 'entity-view.management',
places: [
{
name: 'entity-view.entity-views',
icon: 'view_quilt',
path: '/entityViews'
}
]
}
);
if (authState.edgesSupportEnabled) {
homeSections.push(
{
name: 'edge.management',
places: [
{
name: 'edge.edge-instances',
icon: 'settings_input_antenna',
path: '/edgeInstances'
}
]
}
);
}
homeSections.push(
{
name: 'dashboard.view-dashboards',
places: [
{
name: 'dashboard.dashboards',
icon: 'dashboard',
path: '/dashboards'
}
]
}
);
return homeSections;
}
private allMenuLinks(sections: Array<MenuSection>): Array<MenuSection> { private allMenuLinks(sections: Array<MenuSection>): Array<MenuSection> {
const result: Array<MenuSection> = []; const result: Array<MenuSection> = [];
for (const section of sections) { for (const section of sections) {
@ -1012,6 +91,17 @@ export class MenuService {
return result; return result;
} }
private allMenuSections(sections: Array<MenuSection>): Array<MenuSection> {
const result: Array<MenuSection> = [];
for (const section of sections) {
result.push(section);
if (section.pages && section.pages.length) {
result.push(...this.allMenuSections(section.pages));
}
}
return result;
}
public menuSections(): Observable<Array<MenuSection>> { public menuSections(): Observable<Array<MenuSection>> {
return this.menuSections$; return this.menuSections$;
} }
@ -1024,7 +114,11 @@ export class MenuService {
return this.availableMenuLinks$; return this.availableMenuLinks$;
} }
public menuLinkById(id: string): Observable<MenuSection | undefined> { public availableMenuSections(): Observable<Array<MenuSection>> {
return this.availableMenuSections$;
}
public menuLinkById(id: MenuId | string): Observable<MenuSection | undefined> {
return this.availableMenuLinks$.pipe( return this.availableMenuLinks$.pipe(
map((links) => links.find(link => link.id === id)) map((links) => links.find(link => link.id === id))
); );

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

@ -44,7 +44,8 @@
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[hideHeader]="hideHeader" [hideHeader]="hideHeader"
[scada]="data.scada" [showLayoutConfig]="showLayoutConfig"
[isDefaultBreakpoint]="isDefaultBreakpoint"
isAdd isAdd
formControlName="widgetConfig"> formControlName="widgetConfig">
</tb-widget-config> </tb-widget-config>

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

@ -37,7 +37,8 @@ export interface AddWidgetDialogData {
stateController: IStateController; stateController: IStateController;
widget: Widget; widget: Widget;
widgetInfo: WidgetInfo; widgetInfo: WidgetInfo;
scada: boolean; showLayoutConfig: boolean;
isDefaultBreakpoint: boolean;
} }
@Component({ @Component({
@ -60,6 +61,9 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
stateController: IStateController; stateController: IStateController;
widget: Widget; widget: Widget;
showLayoutConfig = true;
isDefaultBreakpoint = true;
widgetConfig: WidgetConfigComponentData; widgetConfig: WidgetConfigComponentData;
previewMode = false; previewMode = false;
@ -92,6 +96,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.aliasController = this.data.aliasController; this.aliasController = this.data.aliasController;
this.stateController = this.data.stateController; this.stateController = this.data.stateController;
this.widget = this.data.widget; this.widget = this.data.widget;
this.showLayoutConfig = this.data.showLayoutConfig;
this.isDefaultBreakpoint = this.data.isDefaultBreakpoint;
const widgetInfo = this.data.widgetInfo; const widgetInfo = this.data.widgetInfo;

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

@ -29,22 +29,11 @@
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true"> <tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" fxHide.gt-sm displayUserInfo="true">
</tb-user-menu> </tb-user-menu>
<ng-container *ngIf="isEdit"> <ng-container *ngIf="isEdit">
<div style="flex: 1 1 100%" fxHide.gt-sm></div>
<div fxFlex.lt-md fxLayout="row" <div fxFlex.lt-md fxLayout="row"
fxLayoutAlign.gt-sm="start center" fxLayoutAlign.gt-sm="start center"
fxLayoutAlign="end center" fxLayoutGap.lt-lg="6px" fxLayoutGap.gt-md="12px"> fxLayoutAlign="end center" fxLayoutGap.lt-lg="3px" fxLayoutGap.lg="6px" fxLayoutGap.gt-lg="12px">
<button fxHide.gt-md mat-icon-button <mat-divider class="tb-toolbar-divider" vertical></mat-divider>
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardStates($event)">
<mat-icon>layers</mat-icon>
</button>
<button fxHide.lt-lg mat-button
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardStates($event)">
<mat-icon>layers</mat-icon>
{{'dashboard.states-short' | translate}}
</button>
<button fxHide.gt-md mat-icon-button <button fxHide.gt-md mat-icon-button
matTooltip="{{'layout.manage' | translate}}" matTooltip="{{'layout.manage' | translate}}"
matTooltipPosition="below" matTooltipPosition="below"
@ -58,6 +47,9 @@
<mat-icon>view_compact</mat-icon> <mat-icon>view_compact</mat-icon>
{{'layout.layouts' | translate}} {{'layout.layouts' | translate}}
</button> </button>
<tb-select-dashboard-breakpoint
[dashboardCtrl]="this">
</tb-select-dashboard-breakpoint>
</div> </div>
</ng-container> </ng-container>
<tb-states-component fxFlex.lt-md <tb-states-component fxFlex.lt-md
@ -70,6 +62,21 @@
[syncStateWithQueryParam]="syncStateWithQueryParam" [syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states"> [states]="dashboardConfiguration.states">
</tb-states-component> </tb-states-component>
<ng-container *ngIf="isEdit">
<button fxHide.gt-md mat-icon-button
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardStates($event)">
<mat-icon>layers</mat-icon>
</button>
<button fxHide.lt-lg mat-button
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
(click)="manageDashboardStates($event)">
<mat-icon>layers</mat-icon>
{{'dashboard.states-short' | translate}}
</button>
</ng-container>
<img *ngIf="showDashboardLogo()" [src]="dashboardLogo | image | async" <img *ngIf="showDashboardLogo()" [src]="dashboardLogo | image | async"
aria-label="dashboard_logo" class="dashboard_logo"/> aria-label="dashboard_logo" class="dashboard_logo"/>
</div> </div>
@ -88,7 +95,7 @@
(click)="toggleLayouts()"> (click)="toggleLayouts()">
<mat-icon>{{isRightLayoutOpened ? 'arrow_back' : 'menu'}}</mat-icon> <mat-icon>{{isRightLayoutOpened ? 'arrow_back' : 'menu'}}</mat-icon>
</button> </button>
<button [fxShow]="!hideFullscreenButton()" mat-icon-button <button [fxShow]="!hideFullscreenButton()" mat-icon-button fxHide.xs
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}" matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="isFullscreen = !isFullscreen"> (click)="isFullscreen = !isFullscreen">
@ -230,13 +237,13 @@
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-container *ngIf="isEdit"> <ng-container *ngIf="isEdit">
<button fxHide.gt-xs mat-icon-button <button fxHide.gt-md mat-icon-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}" matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="addWidget($event)"> (click)="addWidget($event)">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
<button fxHide.lt-sm mat-stroked-button <button fxHide.lt-lg mat-stroked-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}" matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="addWidget($event)"> (click)="addWidget($event)">
@ -282,7 +289,7 @@
</mat-form-field> </mat-form-field>
</section> </section>
<mat-drawer-container class="tb-dashboard-layouts" fxFlex <mat-drawer-container class="tb-dashboard-layouts" fxFlex
[ngClass]="{ 'tb-shrinked' : isEditingWidget }"> [class]="{'tb-shrinked' : isEditingWidget}">
<mat-drawer *ngIf="layouts.right.show" <mat-drawer *ngIf="layouts.right.show"
id="tb-right-layout" id="tb-right-layout"
[ngStyle]="{minWidth: rightLayoutSize.width, [ngStyle]="{minWidth: rightLayoutSize.width,
@ -308,9 +315,11 @@
</mat-drawer> </mat-drawer>
<mat-drawer-content [fxShow]="layouts.main.show" <mat-drawer-content [fxShow]="layouts.main.show"
id="tb-main-layout" id="tb-main-layout"
[ngStyle]="{width: mainLayoutSize.width, [style]="{width: mainLayoutSize.width,
height: mainLayoutSize.height}"> height: mainLayoutSize.height}">
<tb-dashboard-layout <tb-dashboard-layout
[style.max-width]="mainLayoutSize.maxWidth"
[style.min-width]="mainLayoutSize.minWidth"
[dashboardCheatSheet]="cheatSheetComponent" [dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx" [layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx" [dashboardCtx]="dashboardCtx"
@ -321,6 +330,11 @@
[parentDashboard]="parentDashboard" [parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent"> [popoverComponent]="popoverComponent">
</tb-dashboard-layout> </tb-dashboard-layout>
<div class="layout-filler"
*ngIf="isEdit"
[fxShow]="showMainLayoutFiller">
{{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }}
</div>
</mat-drawer-content> </mat-drawer-content>
</mat-drawer-container> </mat-drawer-container>
<section data-html2canvas-ignore fxLayout="row" class="layout-wrap tb-header-buttons tb-enter-edit-mode" fxLayoutAlign="start end" *ngIf="!readonly && (hideToolbar || widgetEditMode)"> <section data-html2canvas-ignore fxLayout="row" class="layout-wrap tb-header-buttons tb-enter-edit-mode" fxLayoutAlign="start end" *ngIf="!readonly && (hideToolbar || widgetEditMode)">
@ -380,7 +394,8 @@
[widgetEditMode]="widgetEditMode" [widgetEditMode]="widgetEditMode"
[widget]="editingWidget" [widget]="editingWidget"
[widgetLayout]="editingWidgetLayout" [widgetLayout]="editingWidgetLayout"
[scada]="editingLayoutCtx?.gridSettings?.layoutType === LayoutType.scada" [showLayoutConfig]="showLayoutConfigInEdit(editingLayoutCtx)"
[isDefaultBreakpoint]="editingLayoutCtx?.breakpoint === 'default'"
(revertWidgetConfig)="onRevertWidgetEdit()" (revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()"> (applyWidgetConfig)="saveWidget()">
</tb-edit-widget> </tb-edit-widget>

20
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.scss

@ -131,6 +131,26 @@ div.tb-dashboard-page {
width: max(40%, calc(100% - 1192px)); width: max(40%, calc(100% - 1192px));
} }
} }
#tb-main-layout {
display: flex;
tb-dashboard-layout {
width: 100%;
}
.layout-filler {
flex: 1;
background: repeating-linear-gradient(-16deg, #d3d3d3, #d3d3d3 3px, #e0e0e0 3px, #e0e0e0 11px);
border-bottom: 2px dashed #bdbdbd;
border-right: 2px dashed #bdbdbd;
writing-mode: vertical-rl;
text-align: center;
transform: rotate(180deg);
color: #616161;
letter-spacing: .25px;
}
}
} }
mat-drawer.tb-details-drawer { mat-drawer.tb-details-drawer {

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

@ -40,8 +40,9 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import { AuthService } from '@core/auth/auth.service';
import { import {
BreakpointId,
BreakpointInfo,
Dashboard, Dashboard,
DashboardConfiguration, DashboardConfiguration,
DashboardLayoutId, DashboardLayoutId,
@ -52,6 +53,7 @@ import {
GridSettings, GridSettings,
LayoutDimension, LayoutDimension,
LayoutType, LayoutType,
ViewFormatType,
WidgetLayout WidgetLayout
} from '@app/shared/models/dashboard.models'; } from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
@ -67,7 +69,7 @@ import {
IDashboardController, IDashboardController,
LayoutWidgetsArray LayoutWidgetsArray
} from './dashboard-page.models'; } from './dashboard-page.models';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model'; import { AuthUser } from '@shared/models/user.model';
import { getCurrentAuthState } from '@core/auth/auth.selectors'; import { getCurrentAuthState } from '@core/auth/auth.selectors';
@ -84,7 +86,7 @@ import { Authority } from '@shared/models/authority.enum';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { AliasController } from '@core/api/alias-controller'; import { AliasController } from '@core/api/alias-controller';
import { Observable, of, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, of, Subject, Subscription, throwError } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { DashboardService } from '@core/http/dashboard.service'; import { DashboardService } from '@core/http/dashboard.service';
import { import {
@ -93,7 +95,6 @@ import {
WidgetContextMenuItem WidgetContextMenuItem
} from '../../models/dashboard-component.models'; } from '../../models/dashboard-component.models';
import { WidgetComponentService } from '../../components/widget/widget-component.service'; import { WidgetComponentService } from '../../components/widget/widget-component.service';
import { UntypedFormBuilder } from '@angular/forms';
import { ItemBufferService } from '@core/services/item-buffer.service'; import { ItemBufferService } from '@core/services/item-buffer.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { import {
@ -139,14 +140,14 @@ import {
DashboardImageDialogData, DashboardImageDialogData,
DashboardImageDialogResult DashboardImageDialogResult
} from '@home/components/dashboard-page/dashboard-image-dialog.component'; } from '@home/components/dashboard-page/dashboard-image-dialog.component';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; import { SafeUrl } from '@angular/platform-browser';
import cssjs from '@core/css/css'; import cssjs from '@core/css/css';
import { DOCUMENT } from '@angular/common'; import { DOCUMENT } from '@angular/common';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { MatButton } from '@angular/material/button'; import { MatButton } from '@angular/material/button';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { map, tap } from 'rxjs/operators'; import { catchError, distinctUntilChanged, map, skip, tap } from 'rxjs/operators';
import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models'; import { LayoutFixedSize, LayoutWidthType } from '@home/components/dashboard-page/layout/layout.models';
import { TbPopoverComponent } from '@shared/components/popover.component'; import { TbPopoverComponent } from '@shared/components/popover.component';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
@ -155,6 +156,7 @@ import {
MoveWidgetsDialogComponent, MoveWidgetsDialogComponent,
MoveWidgetsDialogResult MoveWidgetsDialogResult
} from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; } from '@home/components/dashboard-page/layout/move-widgets-dialog.component';
import { HttpStatusCode } from '@angular/common/http';
// @dynamic // @dynamic
@Component({ @Component({
@ -262,7 +264,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
addingLayoutCtx: DashboardPageLayoutContext; addingLayoutCtx: DashboardPageLayoutContext;
mainLayoutSize: {width: string; height: string} = {width: '100%', height: '100%'}; mainLayoutSize: {width: string; height: string; maxWidth: string; minWidth: string} =
{width: '100%', height: '100%', maxWidth: '100%', minWidth: '100%'};
rightLayoutSize: {width: string; height: string} = {width: '100%', height: '100%'}; rightLayoutSize: {width: string; height: string} = {width: '100%', height: '100%'};
private dashboardLogoCache: SafeUrl; private dashboardLogoCache: SafeUrl;
@ -275,6 +278,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
getDashboard: () => this.dashboard, getDashboard: () => this.dashboard,
dashboardTimewindow: null, dashboardTimewindow: null,
state: null, state: null,
breakpoint: null,
stateController: null, stateController: null,
stateChanged: null, stateChanged: null,
stateId: null, stateId: null,
@ -287,26 +291,32 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
show: false, show: false,
layoutCtx: { layoutCtx: {
id: 'main', id: 'main',
breakpoint: 'default',
widgets: null, widgets: null,
widgetLayouts: {}, widgetLayouts: {},
gridSettings: {}, gridSettings: {},
ignoreLoading: true, ignoreLoading: true,
ctrl: null, ctrl: null,
dashboardCtrl: this, dashboardCtrl: this,
displayGrid: 'onDrag&Resize' displayGrid: 'onDrag&Resize',
layoutData: null,
layoutDataChanged: new BehaviorSubject(null),
} }
}, },
right: { right: {
show: false, show: false,
layoutCtx: { layoutCtx: {
id: 'right', id: 'right',
breakpoint: 'default',
widgets: null, widgets: null,
widgetLayouts: {}, widgetLayouts: {},
gridSettings: {}, gridSettings: {},
ignoreLoading: true, ignoreLoading: true,
ctrl: null, ctrl: null,
dashboardCtrl: this, dashboardCtrl: this,
displayGrid: 'onDrag&Resize' displayGrid: 'onDrag&Resize',
layoutData: null,
layoutDataChanged: new BehaviorSubject(null),
} }
} }
}; };
@ -340,6 +350,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
@ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent; @ViewChild('dashboardWidgetSelect') dashboardWidgetSelectComponent: DashboardWidgetSelectComponent;
private changeMobileSize = new Subject<boolean>();
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window, @Inject(WINDOW) private window: Window,
@Inject(DOCUMENT) private document: Document, @Inject(DOCUMENT) private document: Document,
@ -348,7 +360,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private router: Router, private router: Router,
private utils: UtilsService, private utils: UtilsService,
private dashboardUtils: DashboardUtilsService, private dashboardUtils: DashboardUtilsService,
private authService: AuthService,
private entityService: EntityService, private entityService: EntityService,
private dialogService: DialogService, private dialogService: DialogService,
private widgetComponentService: WidgetComponentService, private widgetComponentService: WidgetComponentService,
@ -356,7 +367,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private itembuffer: ItemBufferService, private itembuffer: ItemBufferService,
private importExport: ImportExportService, private importExport: ImportExportService,
private mobileService: MobileService, private mobileService: MobileService,
private fb: UntypedFormBuilder,
private dialog: MatDialog, private dialog: MatDialog,
public translate: TranslateService, public translate: TranslateService,
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
@ -366,12 +376,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private overlay: Overlay, private overlay: Overlay,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer,
public elRef: ElementRef, public elRef: ElementRef,
private injector: Injector) { private injector: Injector) {
super(store); super(store);
if (isDefinedAndNotNull(embeddedValue)) { if (isDefinedAndNotNull(this.embeddedValue)) {
this.embedded = embeddedValue; this.embedded = this.embeddedValue;
} }
} }
@ -411,13 +420,42 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
)); ));
} }
this.rxSubscriptions.push(this.breakpointObserver this.rxSubscriptions.push(
.observe(MediaBreakpoints['gt-sm']) this.changeMobileSize.pipe(
.subscribe((state: BreakpointState) => { distinctUntilChanged(),
this.isMobile = !state.matches; ).subscribe((state) => {
this.isMobile = state;
this.updateLayoutSizes();
})
);
this.rxSubscriptions.push(
this.breakpointObserver.observe(
this.dashboardUtils.getBreakpoints()
).pipe(
map(value => this.parseBreakpointsResponse(value.breakpoints)),
tap((value) => {
this.dashboardCtx.breakpoint = value.id;
this.changeMobileSize.next(this.isMobileSize(value));
}),
distinctUntilChanged((_, next) => {
if (this.layouts.right.show || this.isEdit) {
return true;
}
let nextBreakpointConfiguration: BreakpointId = 'default';
if (!!this.layouts.main.layoutCtx.layoutData?.[next.id]) {
nextBreakpointConfiguration = next.id;
}
return this.layouts.main.layoutCtx.breakpoint === nextBreakpointConfiguration;
}),
skip(1)
).subscribe(() => {
this.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint();
this.updateLayoutSizes(); this.updateLayoutSizes();
} }
)); )
);
if (this.isMobileApp && this.syncStateWithQueryParam) { if (this.isMobileApp && this.syncStateWithQueryParam) {
this.mobileService.registerToggleLayoutFunction(() => { this.mobileService.registerToggleLayoutFunction(() => {
setTimeout(() => { setTimeout(() => {
@ -565,6 +603,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if (this.dashboardResize$) { if (this.dashboardResize$) {
this.dashboardResize$.disconnect(); this.dashboardResize$.disconnect();
} }
this.layouts.main.layoutCtx.layoutDataChanged.unsubscribe();
this.layouts.right.layoutCtx.layoutDataChanged.unsubscribe();
} }
public runChangeDetection() { public runChangeDetection() {
@ -588,7 +628,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
public hideFullscreenButton(): boolean { public hideFullscreenButton(): boolean {
return (this.widgetEditMode || this.iframeMode || this.forceFullscreen || this.singlePageMode || this.isEdit); return (this.widgetEditMode || this.iframeMode || this.forceFullscreen || this.singlePageMode);
} }
public toolbarAlwaysOpen(): boolean { public toolbarAlwaysOpen(): boolean {
@ -703,7 +743,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened); this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened);
} }
private updateLayoutSizes() { public updateLayoutSizes() {
let changeMainLayoutSize = false; let changeMainLayoutSize = false;
let changeRightLayoutSize = false; let changeRightLayoutSize = false;
if (this.dashboardCtx.state) { if (this.dashboardCtx.state) {
@ -718,6 +758,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private updateMainLayoutSize(): boolean { private updateMainLayoutSize(): boolean {
const prevMainLayoutWidth = this.mainLayoutSize.width; const prevMainLayoutWidth = this.mainLayoutSize.width;
const prevMainLayoutHeight = this.mainLayoutSize.height; const prevMainLayoutHeight = this.mainLayoutSize.height;
const prevMainLayoutMaxWidth = this.mainLayoutSize.maxWidth;
if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') { if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') {
this.mainLayoutSize.width = '100%'; this.mainLayoutSize.width = '100%';
} else { } else {
@ -728,7 +769,31 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} else { } else {
this.mainLayoutSize.height = '0px'; this.mainLayoutSize.height = '0px';
} }
return prevMainLayoutWidth !== this.mainLayoutSize.width || prevMainLayoutHeight !== this.mainLayoutSize.height; if (this.isEdit && !this.isEditingWidget) {
const xOffset = this.dashboardContainer.nativeElement.getBoundingClientRect().x;
const breakpoint = this.dashboardUtils.getBreakpointInfoById(this.layouts.main.layoutCtx.breakpoint);
let maxWidth: string;
if (breakpoint?.maxWidth) {
if (this.isMobileSize(breakpoint)) {
maxWidth = `${breakpoint.maxWidth}px`;
} else {
maxWidth = `${breakpoint.maxWidth - xOffset}px`;
}
} else {
maxWidth = '100%';
}
const minWidth = breakpoint?.minWidth ? `${breakpoint.minWidth}px` : undefined;
this.mainLayoutSize.maxWidth = maxWidth;
this.mainLayoutSize.minWidth = minWidth;
} else {
this.mainLayoutSize.maxWidth = '100%';
this.mainLayoutSize.minWidth = undefined;
}
return prevMainLayoutWidth !== this.mainLayoutSize.width || prevMainLayoutHeight !== this.mainLayoutSize.height ||
prevMainLayoutMaxWidth !== this.mainLayoutSize.maxWidth;
} }
private updateRightLayoutSize(): boolean { private updateRightLayoutSize(): boolean {
@ -864,6 +929,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: { data: {
settings: deepClone(this.dashboard.configuration.settings), settings: deepClone(this.dashboard.configuration.settings),
gridSettings, gridSettings,
breakpointId: this.layouts.main.layoutCtx.breakpoint
} }
}).afterClosed().subscribe((data) => { }).afterClosed().subscribe((data) => {
if (data) { if (data) {
@ -1027,7 +1093,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
public saveDashboard() { public saveDashboard() {
this.translatedDashboardTitle = this.getTranslatedDashboardTitle(); this.translatedDashboardTitle = this.getTranslatedDashboardTitle();
this.setEditMode(false, false);
this.notifyDashboardUpdated(); this.notifyDashboardUpdated();
} }
@ -1056,21 +1121,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.updateLayout(layout, layoutInfo); this.updateLayout(layout, layoutInfo);
} else { } else {
layout.show = false; layout.show = false;
this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null}); this.updateLayout(layout, {default: {widgetIds: [], widgetLayouts: {}, gridSettings: null}});
} }
} }
} }
private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) { private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) {
if (layoutInfo.gridSettings) { layout.layoutCtx.layoutData = layoutInfo;
layout.layoutCtx.gridSettings = layoutInfo.gridSettings; layout.layoutCtx.layoutDataChanged.next();
} layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(this.isEdit ? layout.layoutCtx.breakpoint : null, layout.show);
layout.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds);
layout.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
if (layout.show && layout.layoutCtx.ctrl) {
layout.layoutCtx.ctrl.reload();
}
layout.layoutCtx.ignoreLoading = true;
this.updateLayoutSizes(); this.updateLayoutSizes();
} }
@ -1145,8 +1204,33 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: widget data: widget
}; };
this.window.parent.postMessage(JSON.stringify(message), '*'); this.window.parent.postMessage(JSON.stringify(message), '*');
this.setEditMode(false, false);
} else { } else {
this.dashboardService.saveDashboard(this.dashboard).subscribe(); let reInitDashboard = false;
this.dashboardService.saveDashboard(this.dashboard).pipe(
catchError((err) => {
if (err.status === HttpStatusCode.Conflict) {
reInitDashboard = true;
return this.dashboardService.getDashboard(this.dashboard.id.id).pipe(
map(dashboard => this.dashboardUtils.validateAndUpdateDashboard(dashboard))
);
}
return throwError(() => err);
})
).subscribe((dashboard) => {
if (reInitDashboard) {
const dashboardPageInitData: DashboardPageInitData = {
dashboard,
currentDashboardId: dashboard.id ? dashboard.id.id : null,
widgetEditMode: this.widgetEditMode,
singlePageMode: this.singlePageMode
};
this.init(dashboardPageInitData);
} else {
this.dashboard = dashboard;
this.setEditMode(false, false);
}
});
} }
} }
@ -1189,8 +1273,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) { private addWidgetToLayout(widget: Widget, layoutId: DashboardLayoutId) {
this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget); const layoutCtx = this.layouts[layoutId].layoutCtx;
this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id); this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget, undefined,
undefined, -1, -1, layoutCtx.breakpoint);
layoutCtx.widgets.addWidgetId(widget.id);
this.runChangeDetection(); this.runChangeDetection();
} }
@ -1250,10 +1336,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
col: 0 col: 0
}; };
newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget); newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget);
let isDefaultBreakpoint = true;
if (this.addingLayoutCtx?.breakpoint) {
isDefaultBreakpoint = this.addingLayoutCtx.breakpoint === 'default';
} else if (!this.layouts.right.show) {
isDefaultBreakpoint = this.layouts.main.layoutCtx.breakpoint === 'default';
}
const scada = this.isAddingToScadaLayout(); const scada = this.isAddingToScadaLayout();
if (scada) { if (scada) {
newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget); newWidget = this.dashboardUtils.prepareWidgetForScadaLayout(newWidget);
} }
let showLayoutConfig = true;
if (scada || this.layouts.right.show || !this.showLayoutConfigInEdit(this.layouts.main.layoutCtx)) {
showLayoutConfig = false;
}
if (widgetTypeInfo.typeParameters.useCustomDatasources) { if (widgetTypeInfo.typeParameters.useCustomDatasources) {
this.addWidgetToDashboard(newWidget); this.addWidgetToDashboard(newWidget);
} else { } else {
@ -1269,7 +1365,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
stateController: this.dashboardCtx.stateController, stateController: this.dashboardCtx.stateController,
widget: newWidget, widget: newWidget,
widgetInfo: widgetTypeInfo, widgetInfo: widgetTypeInfo,
scada showLayoutConfig,
isDefaultBreakpoint
} }
}).afterClosed().subscribe((addedWidget) => { }).afterClosed().subscribe((addedWidget) => {
if (addedWidget) { if (addedWidget) {
@ -1348,18 +1445,38 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
} }
showLayoutConfigInEdit(layoutCtx: DashboardPageLayoutContext): boolean {
return layoutCtx?.gridSettings?.layoutType === LayoutType.divider ||
layoutCtx?.gridSettings?.layoutType === LayoutType.default &&
(layoutCtx?.breakpoint === 'default' ||
layoutCtx?.breakpoint !== 'default' && layoutCtx?.gridSettings?.viewFormat === ViewFormatType.list);
}
replaceReferenceWithWidgetCopy($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
$event.stopPropagation();
const isRemove = layoutCtx.widgets.removeWidgetId(widget.id);
const widgetCopy = this.dashboardUtils.replaceReferenceWithWidgetCopy(widget, this.dashboard, this.dashboardCtx.state,
layoutCtx.id, layoutCtx.breakpoint, isRemove);
layoutCtx.widgets.addWidgetId(widgetCopy.id);
this.runChangeDetection();
}
copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
this.itembuffer.copyWidget(this.dashboard, this.itembuffer.copyWidget(this.dashboard,
this.dashboardCtx.state, layoutCtx.id, widget); this.dashboardCtx.state, layoutCtx.id, widget, layoutCtx.breakpoint);
} }
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) { copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget) {
this.itembuffer.copyWidgetReference(this.dashboard, this.itembuffer.copyWidgetReference(this.dashboard,
this.dashboardCtx.state, layoutCtx.id, widget); this.dashboardCtx.state, layoutCtx.id, widget, layoutCtx.breakpoint);
} }
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
this.itembuffer.pasteWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, this.itembuffer.pasteWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, layoutCtx.breakpoint,
pos, this.entityAliasesUpdated.bind(this), this.filtersUpdated.bind(this)).subscribe( pos, this.entityAliasesUpdated.bind(this), this.filtersUpdated.bind(this)).subscribe(
(widget) => { (widget) => {
layoutCtx.widgets.addWidgetId(widget.id); layoutCtx.widgets.addWidgetId(widget.id);
@ -1368,7 +1485,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) { pasteWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition) {
this.itembuffer.pasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id, this.itembuffer.pasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id, layoutCtx.breakpoint,
pos).subscribe( pos).subscribe(
(widget) => { (widget) => {
layoutCtx.widgets.addWidgetId(widget.id); layoutCtx.widgets.addWidgetId(widget.id);
@ -1390,7 +1507,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
).subscribe((res) => { ).subscribe((res) => {
if (res) { if (res) {
if (layoutCtx.widgets.removeWidgetId(widget.id)) { if (layoutCtx.widgets.removeWidgetId(widget.id)) {
this.dashboardUtils.removeWidgetFromLayout(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget.id); this.dashboardUtils.removeWidgetFromLayout(this.dashboard, this.dashboardCtx.state, layoutCtx.id,
widget.id, layoutCtx.breakpoint);
this.runChangeDetection(); this.runChangeDetection();
} }
} }
@ -1399,7 +1517,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string) { exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string) {
$event.stopPropagation(); $event.stopPropagation();
this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget, widgetTitle); this.importExport.exportWidget(this.dashboard, this.dashboardCtx.state, layoutCtx.id, widget, widgetTitle, layoutCtx.breakpoint);
} }
dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext) { dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext) {
@ -1455,7 +1573,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
action: ($event) => { action: ($event) => {
layoutCtx.ctrl.pasteWidgetReference($event); layoutCtx.ctrl.pasteWidgetReference($event);
}, },
enabled: this.itembuffer.canPasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id), enabled: this.itembuffer.canPasteWidgetReference(this.dashboard, this.dashboardCtx.state, layoutCtx.id, layoutCtx.breakpoint),
value: 'action.paste-reference', value: 'action.paste-reference',
icon: 'content_paste', icon: 'content_paste',
shortcut: 'M-I' shortcut: 'M-I'
@ -1475,7 +1593,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
return dashboardContextActions; return dashboardContextActions;
} }
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem> { prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem> {
const widgetContextActions: Array<WidgetContextMenuItem> = []; const widgetContextActions: Array<WidgetContextMenuItem> = [];
if (this.isEdit && !this.isEditingWidget) { if (this.isEdit && !this.isEditingWidget) {
widgetContextActions.push( widgetContextActions.push(
@ -1488,6 +1606,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
icon: 'edit' icon: 'edit'
} }
); );
if (isReference) {
widgetContextActions.push(
{
action: (event, currentWidget) => {
this.replaceReferenceWithWidgetCopy(event, layoutCtx, currentWidget);
},
enabled: true,
value: 'widget.replace-reference-with-widget-copy',
icon: 'mdi:file-replace-outline'
}
);
}
if (!this.widgetEditMode) { if (!this.widgetEditMode) {
widgetContextActions.push( widgetContextActions.push(
{ {
@ -1646,4 +1776,31 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}); });
} }
} }
get showMainLayoutFiller(): boolean {
const layoutMaxWidth = this.dashboardUtils.getBreakpointInfoById(this.layouts.main.layoutCtx.breakpoint)?.maxWidth || Infinity;
const dashboardMaxWidth = this.dashboardUtils.getBreakpointInfoById(this.dashboardCtx.breakpoint)?.maxWidth || Infinity;
return !this.layouts.right.show && layoutMaxWidth < dashboardMaxWidth && !this.isEditingWidget;
}
get currentBreakpointValue(): string {
return this.dashboardUtils.getBreakpointSizeDescription(this.layouts.main.layoutCtx.breakpoint);
}
private parseBreakpointsResponse(breakpoints: {[key: string]: boolean}): BreakpointInfo {
const activeBreakpoints: BreakpointInfo[] = [];
Object.keys(breakpoints).map((key) => {
if (breakpoints[key]) {
activeBreakpoints.push(this.dashboardUtils.getBreakpointInfoByValue(key));
}
});
return activeBreakpoints.pop();
}
private isMobileSize(breakpoint: BreakpointInfo): boolean {
if (breakpoint?.maxWidth) {
return breakpoint.maxWidth < 960;
}
return false;
}
} }

21
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.models.ts

@ -14,14 +14,22 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Dashboard, DashboardLayoutId, GridSettings, WidgetLayouts } from '@app/shared/models/dashboard.models'; import {
BreakpointId,
Dashboard,
DashboardLayoutId,
DashboardLayoutInfo,
GridSettings,
WidgetLayouts
} from '@app/shared/models/dashboard.models';
import { Widget, WidgetPosition } from '@app/shared/models/widget.models'; import { Widget, WidgetPosition } from '@app/shared/models/widget.models';
import { Timewindow } from '@shared/models/time/time.models'; import { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models'; import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { ILayoutController } from './layout/layout.models'; import { ILayoutController } from './layout/layout.models';
import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models'; import { DashboardContextMenuItem, WidgetContextMenuItem } from '@home/models/dashboard-component.models';
import { Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface'; import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
import { ElementRef } from '@angular/core';
export declare type DashboardPageScope = 'tenant' | 'customer'; export declare type DashboardPageScope = 'tenant' | 'customer';
@ -35,6 +43,7 @@ export interface DashboardPageInitData {
export interface DashboardContext { export interface DashboardContext {
instanceId: string; instanceId: string;
state: string; state: string;
breakpoint: BreakpointId;
getDashboard: () => Dashboard; getDashboard: () => Dashboard;
dashboardTimewindow: Timewindow; dashboardTimewindow: Timewindow;
aliasController: IAliasController; aliasController: IAliasController;
@ -46,17 +55,20 @@ export interface DashboardContext {
export interface IDashboardController { export interface IDashboardController {
dashboardCtx: DashboardContext; dashboardCtx: DashboardContext;
dashboardContainer: ElementRef;
elRef: ElementRef;
openRightLayout(); openRightLayout();
openDashboardState(stateId: string, openRightLayout: boolean); openDashboardState(stateId: string, openRightLayout: boolean);
addWidget($event: Event, layoutCtx: DashboardPageLayoutContext); addWidget($event: Event, layoutCtx: DashboardPageLayoutContext);
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
replaceReferenceWithWidgetCopy($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string); exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string);
removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext); dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext);
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem>; prepareDashboardContextMenu(layoutCtx: DashboardPageLayoutContext): Array<DashboardContextMenuItem>;
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem>; prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem>;
copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); copyWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget); copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition); pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition);
@ -65,6 +77,9 @@ export interface IDashboardController {
export interface DashboardPageLayoutContext { export interface DashboardPageLayoutContext {
id: DashboardLayoutId; id: DashboardLayoutId;
layoutData: DashboardLayoutInfo;
layoutDataChanged: BehaviorSubject<void>;
breakpoint: BreakpointId;
widgets: LayoutWidgetsArray; widgets: LayoutWidgetsArray;
widgetLayouts: WidgetLayouts; widgetLayouts: WidgetLayouts;
gridSettings: GridSettings; gridSettings: GridSettings;

43
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

@ -105,18 +105,18 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
<div *ngIf="gridSettings" class="tb-form-panel no-border no-padding" [formGroup]="gridSettingsFormGroup"> <div *ngIf="gridSettings" class="tb-form-panel no-border no-padding" [formGroup]="gridSettingsFormGroup">
<div class="tb-form-row space-between">
<div class="fixed-title-width" translate>dashboard.layout</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="layoutType">
<mat-option *ngFor="let type of layoutTypes" [value]="type">
{{ layoutTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.layout-settings</div> <div class="tb-form-panel-title" translate [translateParams]="{type: layoutSettingsType}">dashboard.layout-settings-type</div>
<div *ngIf="isDefaultLayout && !isDefaultBreakpoint" class="tb-form-row space-between">
<div class="fixed-title-width" translate>dashboard.view-format</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="viewFormat">
<mat-option *ngFor="let type of viewFormatTypes" [value]="type">
{{ viewFormatTypeTranslationMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>dashboard.columns-count</div> <div translate>dashboard.columns-count</div>
<mat-form-field *ngIf="!isScada()" <mat-form-field *ngIf="!isScada()"
@ -188,6 +188,25 @@
{{ 'dashboard.autofill-height' | translate }} {{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div *ngIf="isDefaultLayout && !isDefaultBreakpoint" class="tb-form-row space-between">
<div translate>dashboard.row-height</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<input matInput formControlName="rowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate"
*ngIf="gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched"
class="tb-error">
warning
</mat-icon>
<span matSuffix>px</span>
</mat-form-field>
</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>dashboard.background-color</div> <div translate>dashboard.background-color</div>
<tb-color-input asBoxInput <tb-color-input asBoxInput
@ -212,7 +231,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div *ngIf="!isScada() || isRightLayout" class="tb-form-panel stroked"> <div *ngIf="!isScada() && isDefaultBreakpoint" class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.mobile-layout</div> <div class="tb-form-panel-title" translate>dashboard.mobile-layout</div>
<div *ngIf="isRightLayout" class="tb-form-row"> <div *ngIf="isRightLayout" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="mobileDisplayLayoutFirst"> <mat-slide-toggle class="mat-slide" formControlName="mobileDisplayLayoutFirst">

148
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@ -24,20 +24,26 @@ import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { import {
BreakpointId,
DashboardSettings, DashboardSettings,
GridSettings, GridSettings,
LayoutType, LayoutType,
layoutTypes, layoutTypeTranslationMap, StateControllerId,
StateControllerId ViewFormatType,
viewFormatTypes,
viewFormatTypeTranslationMap
} from '@app/shared/models/dashboard.models'; } from '@app/shared/models/dashboard.models';
import { isDefined, isUndefined } from '@core/utils'; import { isDefined, isUndefined } from '@core/utils';
import { StatesControllerService } from './states/states-controller.service'; import { StatesControllerService } from './states/states-controller.service';
import { merge } from 'rxjs'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { merge, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
export interface DashboardSettingsDialogData { export interface DashboardSettingsDialogData {
settings?: DashboardSettings; settings?: DashboardSettings;
gridSettings?: GridSettings; gridSettings?: GridSettings;
isRightLayout?: boolean; isRightLayout?: boolean;
breakpointId?: BreakpointId;
} }
@Component({ @Component({
@ -47,11 +53,10 @@ export interface DashboardSettingsDialogData {
styleUrls: ['./dashboard-settings-dialog.component.scss'] styleUrls: ['./dashboard-settings-dialog.component.scss']
}) })
export class DashboardSettingsDialogComponent extends DialogComponent<DashboardSettingsDialogComponent, DashboardSettingsDialogData> export class DashboardSettingsDialogComponent extends DialogComponent<DashboardSettingsDialogComponent, DashboardSettingsDialogData>
implements OnInit, ErrorStateMatcher { implements OnDestroy, ErrorStateMatcher {
layoutTypes = layoutTypes; viewFormatTypes = viewFormatTypes;
viewFormatTypeTranslationMap = viewFormatTypeTranslationMap;
layoutTypeTranslations = layoutTypeTranslationMap;
settings: DashboardSettings; settings: DashboardSettings;
gridSettings: GridSettings; gridSettings: GridSettings;
@ -62,10 +67,17 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
stateControllerIds: string[]; stateControllerIds: string[];
layoutSettingsType: string;
submitted = false; submitted = false;
scadaColumns: number[] = []; scadaColumns: number[] = [];
private layoutType = LayoutType.default;
private breakpointId: BreakpointId = 'default';
private destroy$ = new Subject<void>();
private stateControllerTranslationMap = new Map<string, string>([ private stateControllerTranslationMap = new Map<string, string>([
['default', 'dashboard.state-controller-default'], ['default', 'dashboard.state-controller-default'],
]); ]);
@ -77,7 +89,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>, public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>,
private fb: FormBuilder, private fb: FormBuilder,
private translate: TranslateService, private translate: TranslateService,
private statesControllerService: StatesControllerService) { private statesControllerService: StatesControllerService,
private dashboardUtils: DashboardUtilsService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers()); this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers());
@ -85,6 +98,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
this.settings = this.data.settings; this.settings = this.data.settings;
this.gridSettings = this.data.gridSettings; this.gridSettings = this.data.gridSettings;
this.isRightLayout = this.data.isRightLayout; this.isRightLayout = this.data.isRightLayout;
this.breakpointId = this.data.breakpointId;
if (this.settings) { if (this.settings) {
const showTitle = isUndefined(this.settings.showTitle) ? true : this.settings.showTitle; const showTitle = isUndefined(this.settings.showTitle) ? true : this.settings.showTitle;
@ -116,14 +130,18 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
disabled: hideToolbar}, []], disabled: hideToolbar}, []],
dashboardCss: [isUndefined(this.settings.dashboardCss) ? '' : this.settings.dashboardCss, []], dashboardCss: [isUndefined(this.settings.dashboardCss) ? '' : this.settings.dashboardCss, []],
}); });
this.settingsFormGroup.get('stateControllerId').valueChanges.subscribe( this.settingsFormGroup.get('stateControllerId').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(stateControllerId: StateControllerId) => { (stateControllerId: StateControllerId) => {
if (stateControllerId !== 'default') { if (stateControllerId !== 'default') {
this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true); this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true);
} }
} }
); );
this.settingsFormGroup.get('showTitle').valueChanges.subscribe( this.settingsFormGroup.get('showTitle').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(showTitleValue: boolean) => { (showTitleValue: boolean) => {
if (showTitleValue) { if (showTitleValue) {
this.settingsFormGroup.get('titleColor').enable(); this.settingsFormGroup.get('titleColor').enable();
@ -132,7 +150,9 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
} }
); );
this.settingsFormGroup.get('showDashboardLogo').valueChanges.subscribe( this.settingsFormGroup.get('showDashboardLogo').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(showDashboardLogoValue: boolean) => { (showDashboardLogoValue: boolean) => {
if (showDashboardLogoValue) { if (showDashboardLogoValue) {
this.settingsFormGroup.get('dashboardLogoUrl').enable(); this.settingsFormGroup.get('dashboardLogoUrl').enable();
@ -141,7 +161,9 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
} }
); );
this.settingsFormGroup.get('hideToolbar').valueChanges.subscribe( this.settingsFormGroup.get('hideToolbar').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(hideToolbarValue: boolean) => { (hideToolbarValue: boolean) => {
if (hideToolbarValue) { if (hideToolbarValue) {
this.settingsFormGroup.get('toolbarAlwaysOpen').disable(); this.settingsFormGroup.get('toolbarAlwaysOpen').disable();
@ -167,6 +189,16 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
if (this.gridSettings) { if (this.gridSettings) {
if (isDefined(this.gridSettings.layoutType)) {
this.layoutType = this.gridSettings.layoutType;
}
if (this.layoutType !== LayoutType.divider) {
this.layoutSettingsType = this.dashboardUtils.getBreakpointName(this.breakpointId);
} else if (this.isRightLayout) {
this.layoutSettingsType = this.translate.instant('layout.right');
} else {
this.layoutSettingsType = this.translate.instant('layout.left');
}
let columns = 24; let columns = 24;
while (columns <= 1008) { while (columns <= 1008) {
this.scadaColumns.push(columns); this.scadaColumns.push(columns);
@ -174,14 +206,16 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
const mobileAutoFillHeight = isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight; const mobileAutoFillHeight = isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight;
this.gridSettingsFormGroup = this.fb.group({ this.gridSettingsFormGroup = this.fb.group({
layoutType: [this.gridSettings.layoutType || LayoutType.default, []],
columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1008)]], columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1008)]],
minColumns: [this.gridSettings.minColumns || this.gridSettings.columns || 24, minColumns: [this.gridSettings.minColumns || this.gridSettings.columns || 24,
[Validators.required, Validators.min(10), Validators.max(1008)]], [Validators.required, Validators.min(10), Validators.max(1008)]],
margin: [isDefined(this.gridSettings.margin) ? this.gridSettings.margin : 10, margin: [isDefined(this.gridSettings.margin) ? this.gridSettings.margin : 10,
[Validators.required, Validators.min(0), Validators.max(50)]], [Validators.required, Validators.min(0), Validators.max(50)]],
outerMargin: [isUndefined(this.gridSettings.outerMargin) ? true : this.gridSettings.outerMargin, []], outerMargin: [isUndefined(this.gridSettings.outerMargin) ? true : this.gridSettings.outerMargin, []],
viewFormat: [isUndefined(this.gridSettings.viewFormat) ? ViewFormatType.grid : this.gridSettings.viewFormat, []],
autoFillHeight: [isUndefined(this.gridSettings.autoFillHeight) ? false : this.gridSettings.autoFillHeight, []], autoFillHeight: [isUndefined(this.gridSettings.autoFillHeight) ? false : this.gridSettings.autoFillHeight, []],
rowHeight: [isUndefined(this.gridSettings.rowHeight) ? 70 : this.gridSettings.rowHeight,
[Validators.required, Validators.min(5), Validators.max(200)]],
backgroundColor: [this.gridSettings.backgroundColor || 'rgba(0,0,0,0)', []], backgroundColor: [this.gridSettings.backgroundColor || 'rgba(0,0,0,0)', []],
backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []], backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []],
backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []], backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []],
@ -194,27 +228,56 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
isUndefined(this.gridSettings.mobileDisplayLayoutFirst) ? false : this.gridSettings.mobileDisplayLayoutFirst; isUndefined(this.gridSettings.mobileDisplayLayoutFirst) ? false : this.gridSettings.mobileDisplayLayoutFirst;
this.gridSettingsFormGroup.addControl('mobileDisplayLayoutFirst', this.fb.control(mobileDisplayLayoutFirst, [])); this.gridSettingsFormGroup.addControl('mobileDisplayLayoutFirst', this.fb.control(mobileDisplayLayoutFirst, []));
} }
merge(this.gridSettingsFormGroup.get('layoutType').valueChanges, if (this.isScada()) {
this.gridSettingsFormGroup.get('mobileAutoFillHeight').valueChanges).subscribe( this.gridSettingsFormGroup.get('margin').disable();
() => { this.gridSettingsFormGroup.get('outerMargin').disable();
this.updateGridSettingsFormState(); this.gridSettingsFormGroup.get('viewFormat').disable({emitEvent: false});
this.gridSettingsFormGroup.get('rowHeight').disable();
this.gridSettingsFormGroup.get('autoFillHeight').disable({emitEvent: false});
this.gridSettingsFormGroup.get('mobileAutoFillHeight').disable({emitEvent: false});
this.gridSettingsFormGroup.get('mobileRowHeight').disable();
const columnsFields: number = this.gridSettingsFormGroup.get('columns').value;
if (columnsFields % 24 !== 0) {
const newColumns = Math.min(1008, 24 * Math.ceil(columnsFields / 24));
this.gridSettingsFormGroup.get('columns').patchValue(newColumns);
} }
); } else {
this.updateGridSettingsFormState(); merge(
this.gridSettingsFormGroup.get('viewFormat').valueChanges,
this.breakpointId !== 'default'
? this.gridSettingsFormGroup.get('autoFillHeight').valueChanges
: this.gridSettingsFormGroup.get('mobileAutoFillHeight').valueChanges
).pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.updateGridSettingsFormState();
});
this.updateGridSettingsFormState();
}
if (this.layoutType === LayoutType.default && this.breakpointId !== 'default' || this.isScada()) {
this.gridSettingsFormGroup.get('mobileAutoFillHeight').disable({emitEvent: false});
this.gridSettingsFormGroup.get('mobileRowHeight').disable();
}
} else { } else {
this.gridSettingsFormGroup = this.fb.group({}); this.gridSettingsFormGroup = this.fb.group({});
} }
} }
ngOnInit(): void { ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
} }
isScada() { isScada() {
if (this.gridSettingsFormGroup) { return this.layoutType === LayoutType.scada;
return this.gridSettingsFormGroup.get('layoutType').value === LayoutType.scada; }
} else {
return false; get isDefaultLayout() {
} return this.layoutType === LayoutType.default;
}
get isDefaultBreakpoint(): boolean {
return this.breakpointId === 'default';
} }
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -248,29 +311,28 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
} }
private updateGridSettingsFormState() { private updateGridSettingsFormState() {
const isScada: boolean = this.gridSettingsFormGroup.get('layoutType').value === LayoutType.scada; if (this.breakpointId === 'default') {
if (isScada) {
this.gridSettingsFormGroup.get('margin').disable();
this.gridSettingsFormGroup.get('outerMargin').disable();
this.gridSettingsFormGroup.get('autoFillHeight').disable();
this.gridSettingsFormGroup.get('mobileAutoFillHeight').disable({emitEvent: false});
this.gridSettingsFormGroup.get('mobileRowHeight').disable();
const columns: number = this.gridSettingsFormGroup.get('columns').value;
if (columns % 24 !== 0) {
const newColumns = Math.min(1008, 24 * Math.ceil(columns / 24));
this.gridSettingsFormGroup.get('columns').patchValue(newColumns);
}
} else {
this.gridSettingsFormGroup.get('margin').enable();
this.gridSettingsFormGroup.get('outerMargin').enable();
this.gridSettingsFormGroup.get('autoFillHeight').enable();
this.gridSettingsFormGroup.get('mobileAutoFillHeight').enable({emitEvent: false});
const mobileAutoFillHeight: boolean = this.gridSettingsFormGroup.get('mobileAutoFillHeight').value; const mobileAutoFillHeight: boolean = this.gridSettingsFormGroup.get('mobileAutoFillHeight').value;
if (mobileAutoFillHeight) { if (mobileAutoFillHeight) {
this.gridSettingsFormGroup.get('mobileRowHeight').disable(); this.gridSettingsFormGroup.get('mobileRowHeight').disable();
} else { } else {
this.gridSettingsFormGroup.get('mobileRowHeight').enable(); this.gridSettingsFormGroup.get('mobileRowHeight').enable();
} }
} else {
const autoFillHeight: boolean = this.gridSettingsFormGroup.get('autoFillHeight').value;
const viewFormat: ViewFormatType = this.gridSettingsFormGroup.get('viewFormat').value;
if (viewFormat !== ViewFormatType.list || autoFillHeight) {
this.gridSettingsFormGroup.get('rowHeight').disable();
} else {
this.gridSettingsFormGroup.get('rowHeight').enable();
}
if (viewFormat === ViewFormatType.list) {
this.gridSettingsFormGroup.get('columns').disable();
this.gridSettingsFormGroup.get('minColumns').disable();
} else {
this.gridSettingsFormGroup.get('columns').enable();
this.gridSettingsFormGroup.get('minColumns').enable();
}
} }
} }
} }

13
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-toolbar.component.scss

@ -174,14 +174,23 @@ tb-dashboard-toolbar {
@media #{$mat-sm} { @media #{$mat-sm} {
gap: 6px; gap: 6px;
&.tb-left-panel {
gap: 3px;
}
} }
@media #{$mat-md} { @media #{$mat-md} {
gap: 6px; gap: 3px;
&.tb-left-panel {
gap: 1px;
}
} }
@media #{$mat-gt-md} { @media #{$mat-gt-md} {
gap: 12px; gap: 6px;
&.tb-left-panel {
gap: 3px;
}
} }
@media #{$mat-gt-sm} { @media #{$mat-gt-sm} {

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

@ -24,7 +24,8 @@
[dashboard]="dashboard" [dashboard]="dashboard"
[widget]="widget" [widget]="widget"
[hideToggleHeader]="previewMode" [hideToggleHeader]="previewMode"
[scada]="scada" [showLayoutConfig]="showLayoutConfig"
[isDefaultBreakpoint]="isDefaultBreakpoint"
formControlName="widgetConfig"> formControlName="widgetConfig">
<div class="tb-widget-config-header-prefix"> <div class="tb-widget-config-header-prefix">
<button mat-button <button mat-button

6
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.ts

@ -60,7 +60,11 @@ export class EditWidgetComponent extends PageComponent implements OnInit, OnChan
@Input() @Input()
@coerceBoolean() @coerceBoolean()
scada = false; showLayoutConfig = true;
@Input()
@coerceBoolean()
isDefaultBreakpoint= true;
@Output() @Output()
applyWidgetConfig = new EventEmitter<void>(); applyWidgetConfig = new EventEmitter<void>();

66
ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html

@ -0,0 +1,66 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-toolbar color="primary">
<h2 translate>layout.add-new-breakpoint</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<div mat-dialog-content style="width: 400px; max-width: 100%;">
<form [formGroup]="addBreakpointFormGroup">
<mat-form-field appearance="outline">
<mat-label translate>layout.breakpoint</mat-label>
<mat-select formControlName="newBreakpointId" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(this.addBreakpointFormGroup.get('newBreakpointId').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of allowBreakpointIds" [value]="breakpoint">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label translate>layout.copy-from</mat-label>
<mat-select formControlName="copyFrom" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(this.addBreakpointFormGroup.get('copyFrom').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of selectedBreakpointIds" [value]="breakpoint">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
</mat-select>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button
color="primary"
type="button"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
<button mat-raised-button color="primary"
type="button"
(click)="save()"
[disabled]="addBreakpointFormGroup.invalid">
{{ 'action.add' | translate }}
</button>
</div>

85
ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.ts

@ -0,0 +1,85 @@
///
/// Copyright © 2016-2024 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, Inject } from '@angular/core';
import { DialogComponent } from '@shared/components/dialog.component';
import { FormBuilder, FormGroup } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { Router } from '@angular/router';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { BreakpointId } from '@shared/models/dashboard.models';
export interface AddNewBreakpointDialogData {
allowBreakpointIds: string[];
selectedBreakpointIds: string[];
}
export interface AddNewBreakpointDialogResult {
newBreakpointId: BreakpointId;
copyFrom: BreakpointId;
}
@Component({
selector: 'add-new-breakpoint-dialog',
templateUrl: './add-new-breakpoint-dialog.component.html',
})
export class AddNewBreakpointDialogComponent extends DialogComponent<AddNewBreakpointDialogComponent, AddNewBreakpointDialogResult> {
addBreakpointFormGroup: FormGroup;
allowBreakpointIds = [];
selectedBreakpointIds = [];
constructor(protected store: Store<AppState>,
protected router: Router,
private fb: FormBuilder,
@Inject(MAT_DIALOG_DATA) private data: AddNewBreakpointDialogData,
protected dialogRef: MatDialogRef<AddNewBreakpointDialogComponent, AddNewBreakpointDialogResult>,
private dashboardUtils: DashboardUtilsService,) {
super(store, router, dialogRef);
this.allowBreakpointIds = this.data.allowBreakpointIds;
this.selectedBreakpointIds = this.data.selectedBreakpointIds;
this.addBreakpointFormGroup = this.fb.group({
newBreakpointId: [{value: this.allowBreakpointIds[0], disabled: this.allowBreakpointIds.length === 1}],
copyFrom: [{value: 'default', disabled: this.selectedBreakpointIds.length === 1}],
});
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.dialogRef.close(this.addBreakpointFormGroup.getRawValue());
}
getName(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointName(breakpointId);
}
getIcon(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointIcon(breakpointId);
}
getSizeDescription(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointSizeDescription(breakpointId);
}
}

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

@ -53,8 +53,8 @@
[isEditingWidget]="isEditingWidget" [isEditingWidget]="isEditingWidget"
[autofillHeight]="autoFillHeight" [autofillHeight]="autoFillHeight"
[mobileAutofillHeight]="mobileAutoFillHeight" [mobileAutofillHeight]="mobileAutoFillHeight"
[mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight" [mobileRowHeight]="mobielRowHeigth"
[isMobile]="isMobile" [isMobile]="isMobileValue"
[isMobileDisabled]="isMobileDisabled" [isMobileDisabled]="isMobileDisabled"
[disableWidgetInteraction]="isEdit" [disableWidgetInteraction]="isEdit"
[isEditActionEnabled]="isEdit && !isEditingWidget" [isEditActionEnabled]="isEdit && !isEditingWidget"

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

@ -37,7 +37,8 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
import { ImagePipe } from '@shared/pipe/image.pipe'; import { ImagePipe } from '@shared/pipe/image.pipe';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface'; import { displayGrids } from 'angular-gridster2/lib/gridsterConfig.interface';
import { LayoutType } from '@shared/models/dashboard.models'; import { BreakpointId, LayoutType, ViewFormatType } from '@shared/models/dashboard.models';
import { isNotEmptyStr } from '@core/utils';
@Component({ @Component({
selector: 'tb-dashboard-layout', selector: 'tb-dashboard-layout',
@ -85,11 +86,27 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
} }
get mobileAutoFillHeight(): boolean { get mobileAutoFillHeight(): boolean {
return (this.isEdit || this.isScada) ? false : this.layoutCtx.gridSettings.mobileAutoFillHeight; if (this.isEdit || this.isScada) {
return false;
} else if (this.layoutCtx.breakpoint !== 'default' && this.layoutCtx.gridSettings.viewFormat === ViewFormatType.list) {
return this.layoutCtx.gridSettings.autoFillHeight;
}
return this.layoutCtx.gridSettings.mobileAutoFillHeight;
}
get isMobileValue(): boolean {
return this.isMobile || (this.layoutCtx.breakpoint !== 'default' && this.layoutCtx.gridSettings.viewFormat === ViewFormatType.list);
} }
get isMobileDisabled(): boolean { get isMobileDisabled(): boolean {
return this.widgetEditMode || this.isScada; return this.widgetEditMode || this.isScada || (this.layoutCtx.breakpoint !== 'default' && !this.isMobileValue);
}
get mobielRowHeigth(): number {
if (this.layoutCtx.breakpoint !== 'default' && this.layoutCtx.gridSettings.viewFormat === ViewFormatType.list) {
return this.layoutCtx.gridSettings.rowHeight;
}
return this.layoutCtx.gridSettings.mobileRowHeight;
} }
get colWidthInteger(): boolean { get colWidthInteger(): boolean {
@ -129,11 +146,13 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
private rxSubscriptions = new Array<Subscription>(); private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>, constructor(
private translate: TranslateService, protected store: Store<AppState>,
private itembuffer: ItemBufferService, private translate: TranslateService,
private imagePipe: ImagePipe, private itembuffer: ItemBufferService,
private sanitizer: DomSanitizer) { private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
) {
super(store); super(store);
this.initHotKeys(); this.initHotKeys();
} }
@ -199,7 +218,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
new Hotkey('ctrl+i', (event: KeyboardEvent) => { new Hotkey('ctrl+i', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) { if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(), if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(),
this.dashboardCtx.state, this.layoutCtx.id)) { this.dashboardCtx.state, this.layoutCtx.id, this.layoutCtx.breakpoint)) {
event.preventDefault(); event.preventDefault();
this.pasteWidgetReference(event); this.pasteWidgetReference(event);
} }
@ -264,6 +283,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget); this.layoutCtx.dashboardCtrl.editWidget($event, this.layoutCtx, widget);
} }
replaceReferenceWithWidgetCopy($event: Event, widget: Widget): void {
this.layoutCtx.dashboardCtrl.replaceReferenceWithWidgetCopy($event, this.layoutCtx, widget);
}
onExportWidget($event: Event, widget: Widget, widgetTitle: string): void { onExportWidget($event: Event, widget: Widget, widgetTitle: string): void {
this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget, widgetTitle); this.layoutCtx.dashboardCtrl.exportWidget($event, this.layoutCtx, widget, widgetTitle);
} }
@ -284,12 +307,12 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget); this.layoutCtx.dashboardCtrl.widgetClicked($event, this.layoutCtx, widget);
} }
prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> { prepareDashboardContextMenu(_: Event): Array<DashboardContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx); return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx);
} }
prepareWidgetContextMenu($event: Event, widget: Widget): Array<WidgetContextMenuItem> { prepareWidgetContextMenu(_: Event, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget); return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget, isReference);
} }
copyWidget($event: Event, widget: Widget) { copyWidget($event: Event, widget: Widget) {
@ -310,4 +333,24 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos); this.layoutCtx.dashboardCtrl.pasteWidgetReference($event, this.layoutCtx, pos);
} }
updatedCurrentBreakpoint(breakpointId?: BreakpointId, showLayout = true) {
if (!isNotEmptyStr(breakpointId)) {
breakpointId = this.dashboardCtx.breakpoint;
}
if (this.layoutCtx.layoutData[breakpointId]) {
this.layoutCtx.breakpoint = breakpointId;
} else {
this.layoutCtx.breakpoint = 'default';
}
const layoutInfo = this.layoutCtx.layoutData[this.layoutCtx.breakpoint];
if (layoutInfo.gridSettings) {
this.layoutCtx.gridSettings = layoutInfo.gridSettings;
}
this.layoutCtx.widgets.setWidgetIds(layoutInfo.widgetIds);
this.layoutCtx.widgetLayouts = layoutInfo.widgetLayouts;
if (showLayout && this.layoutCtx.ctrl) {
this.layoutCtx.ctrl.reload();
}
this.layoutCtx.ignoreLoading = true;
}
} }

1
ui-ngx/src/app/modules/home/components/dashboard-page/layout/layout.models.ts

@ -21,6 +21,7 @@ export interface ILayoutController {
selectWidget(widgetId: string, delay?: number); selectWidget(widgetId: string, delay?: number);
pasteWidget($event: MouseEvent); pasteWidget($event: MouseEvent);
pasteWidgetReference($event: MouseEvent); pasteWidgetReference($event: MouseEvent);
updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean);
} }
export enum LayoutWidthType { export enum LayoutWidthType {

86
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html

@ -25,16 +25,77 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content fxLayout="column" fxLayoutGap="20px" style="min-width: 300px; overflow: hidden;"> <div mat-dialog-content>
<div fxLayout="row" fxLayoutAlign="start center"> <section class="tb-form-panel no-padding no-border">
<mat-slide-toggle formControlName="right" color="accent"> <div class="tb-form-row space-between">
{{ 'layout.divider' | translate }} <div class="fixed-title-width" translate>dashboard.layout</div>
</mat-slide-toggle> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
</div> <mat-select formControlName="layoutType">
<mat-option *ngFor="let type of layoutTypes" [value]="type">
{{ layoutTypeTranslations.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="!isDividerLayout; else dividerLayout" class="tb-form-panel stroked">
<div class="tb-form-table tb-layout-breakpoints">
<div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell tb-icon"></div>
<div class="tb-form-table-header-cell tb-breakpoint" translate>layout.breakpoints</div>
<div class="tb-form-table-header-cell tb-size" translate>layout.size</div>
<div class="tb-form-table-header-cell tb-actions"></div>
</div>
<div class="tb-form-table-body no-gap">
<ng-container *ngFor="let breakpoint of layoutBreakpoints; let last = last">
<div class="tb-form-table-row no-padding-right">
<div class="tb-form-table-row-cell tb-icon">
<tb-icon>{{ breakpoint.icon }}</tb-icon>
</div>
<div class="tb-form-table-row-cell tb-breakpoint">
{{ breakpoint.name }}
</div>
<div class="tb-form-table-row-cell tb-size" tbTruncateWithTooltip>
{{ breakpoint.descriptionSize }}
</div>
<div class="tb-form-table-row-cell-buttons tb-actions">
<button mat-icon-button
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('main', breakpoint.breakpoint)"
type="button" >
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
[disabled]="breakpoint.breakpoint === 'default'"
[class.tb-hidden]="breakpoint.breakpoint === 'default'"
(click)="deleteBreakpoint($event, breakpoint.breakpoint)"
type="button" >
<tb-icon>delete</tb-icon>
</button>
</div>
</div>
<mat-divider *ngIf="!last"></mat-divider>
</ng-container>
</div>
</div>
<button mat-button fxFlex class="tb-add-breakpoint-button"
matTooltip="{{ 'layout.add-new-breakpoint' | translate }}"
matTooltipPosition="above"
color="primary"
type="button"
[disabled]="!allowBreakpointIds.length"
(click)="addBreakpoint()">
<mat-icon>add</mat-icon>
</button>
</div>
</section>
<ng-template #dividerLayout>
<div fxLayout="column" fxLayoutAlign="start center" fxLayoutGap="20px"> <div fxLayout="column" fxLayoutAlign="start center" fxLayoutGap="20px">
<mat-button-toggle-group aria-label="Select width value type" <mat-button-toggle-group aria-label="Select width value type"
formControlName="type" formControlName="type"
[fxShow]="layoutsFormGroup.get('right').value"> [fxShow]="isDividerLayout">
<mat-button-toggle fxFlex value="percentage"> <mat-button-toggle fxFlex value="percentage">
{{ 'layout.percentage-width' | translate }} {{ 'layout.percentage-width' | translate }}
</mat-button-toggle> </mat-button-toggle>
@ -52,7 +113,7 @@
<div fxLayout="column" fxLayoutAlign="start center" fxFlex="{{ buttonFlexValue() }}" class="tb-layout-preview-container-main"> <div fxLayout="column" fxLayoutAlign="start center" fxFlex="{{ buttonFlexValue() }}" class="tb-layout-preview-container-main">
<button mat-icon-button <button mat-icon-button
type="button" type="button"
matTooltip="{{ 'dashboard.layout-settings' | translate }}" matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="openLayoutSettings('main')" (click)="openLayoutSettings('main')"
class="tb-layout-preview-element" class="tb-layout-preview-element"
@ -72,7 +133,7 @@
<span [matTooltip]="layoutButtonText('main')" <span [matTooltip]="layoutButtonText('main')"
#mainLayoutTooltip="matTooltip" #mainLayoutTooltip="matTooltip"
matTooltipPosition="above"> matTooltipPosition="above">
{{ (layoutsFormGroup.value.right ? 'layout.left' : 'layout.main') | translate }} {{ (isDividerLayout ? 'layout.left' : 'layout.main') | translate }}
</span> </span>
</button> </button>
<div fxFlex class="tb-layout-preview-element tb-layout-preview-input" *ngIf="showPreviewInputs('main')"> <div fxFlex class="tb-layout-preview-element tb-layout-preview-input" *ngIf="showPreviewInputs('main')">
@ -93,10 +154,10 @@
required> required>
</div> </div>
</div> </div>
<div fxLayout="column" fxLayoutAlign="start center" fxFlex *ngIf="layoutsFormGroup.get('right').value" matTooltip=""> <div fxLayout="column" fxLayoutAlign="start center" fxFlex *ngIf="isDividerLayout" matTooltip="">
<button mat-icon-button <button mat-icon-button
type="button" type="button"
matTooltip="{{ 'dashboard.layout-settings' | translate }}" matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="openLayoutSettings('right')" (click)="openLayoutSettings('right')"
class="tb-layout-preview-element" class="tb-layout-preview-element"
@ -138,7 +199,7 @@
</div> </div>
</div> </div>
</div> </div>
<div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="8px" *ngIf="layoutsFormGroup.get('right').value" style="width: 80%"> <div fxLayout="column" fxLayoutAlign="center center" fxLayoutGap="8px" *ngIf="isDividerLayout" style="width: 80%">
<mat-slider *ngIf="layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE" <mat-slider *ngIf="layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE"
step="1" step="1"
min="{{ layoutPercentageSize.MIN }}" min="{{ layoutPercentageSize.MIN }}"
@ -167,6 +228,7 @@
</div> </div>
</div> </div>
</div> </div>
</ng-template>
</div> </div>
<div mat-dialog-actions fxLayoutAlign="end center"> <div mat-dialog-actions fxLayoutAlign="end center">
<button mat-button <button mat-button

58
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.scss

@ -15,6 +15,7 @@
*/ */
@use '@angular/material' as mat; @use '@angular/material' as mat;
@import '../theme.scss'; @import '../theme.scss';
@import '../scss/constants.scss';
$tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text); $tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text);
@ -33,7 +34,7 @@ $tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text);
} }
.tb-layout-preview { .tb-layout-preview {
width: 120%; width: 100%;
background-color: rgba(mat.get-color-from-palette($tb-primary, 50), 0.6); background-color: rgba(mat.get-color-from-palette($tb-primary, 50), 0.6);
padding: 35px; padding: 35px;
@ -121,6 +122,53 @@ $tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text);
} }
} }
} }
.tb-layout-breakpoints {
.tb-form-table-header-cell, .tb-form-table-row-cell {
&.tb-icon {
width: 29px;
min-width: 29px;
max-width: 29px;
display: flex;
place-content: center;
}
&.tb-breakpoint {
flex: 1 1 15%;
width: 15%;
min-width: 100px;
}
&.tb-size {
flex: 1 1 85%;
width: 200px;
}
&.tb-actions {
width: 96pxpx;
min-width: 96px;
max-width: 96px;
display: flex;
justify-content: end;
}
}
.mat-divider {
margin-top: 6px;
margin-bottom: 6px;
}
}
.tb-add-breakpoint-button {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #FFFFFF;
padding: 8px;
border: 2px dashed rgba(0, 0, 0, 0.08);
border-radius: 10px;
}
} }
:host ::ng-deep { :host ::ng-deep {
@ -137,6 +185,14 @@ $tb-warn: mat.get-color-from-palette(map-get($tb-theme, warn), text);
} }
} }
:host-context(.tb-fullscreen-dialog .mat-mdc-dialog-container) {
.mat-mdc-dialog-content {
width: 550px;
max-width: 100%;
padding: 16px;
}
}
::ng-deep { ::ng-deep {
/* Alarm tooltip with side-to-side movement */ /* Alarm tooltip with side-to-side movement */
.tb-layout-error-tooltip-right { .tb-layout-error-tooltip-right {

227
ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.ts

@ -14,26 +14,35 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, Inject, SkipSelf, ViewChild } from '@angular/core'; import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core'; import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { import {
AbstractControl, AbstractControl,
FormGroupDirective,
NgForm,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component'; import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DashboardLayoutId, DashboardStateLayouts, LayoutDimension } from '@app/shared/models/dashboard.models'; import {
import { deepClone, isDefined } from '@core/utils'; BreakpointId,
DashboardLayout,
DashboardLayoutId,
DashboardStateLayouts,
LayoutDimension,
LayoutType,
layoutTypes,
layoutTypeTranslationMap,
ViewFormatType
} from '@app/shared/models/dashboard.models';
import { deepClone, isDefined, isEqual } from '@core/utils';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { import {
DashboardSettingsDialogComponent, DashboardSettingsDialogComponent,
@ -46,11 +55,25 @@ import {
} from '@home/components/dashboard-page/layout/layout.models'; } from '@home/components/dashboard-page/layout/layout.models';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import {
AddNewBreakpointDialogComponent,
AddNewBreakpointDialogData,
AddNewBreakpointDialogResult
} from '@home/components/dashboard-page/layout/add-new-breakpoint-dialog.component';
import { DialogService } from '@core/services/dialog.service';
export interface ManageDashboardLayoutsDialogData { export interface ManageDashboardLayoutsDialogData {
layouts: DashboardStateLayouts; layouts: DashboardStateLayouts;
} }
export interface DashboardLayoutSettings {
icon: string;
name: string;
descriptionSize?: string;
layout: DashboardLayout;
breakpoint: BreakpointId;
}
@Component({ @Component({
selector: 'tb-manage-dashboard-layouts-dialog', selector: 'tb-manage-dashboard-layouts-dialog',
templateUrl: './manage-dashboard-layouts-dialog.component.html', templateUrl: './manage-dashboard-layouts-dialog.component.html',
@ -58,9 +81,9 @@ export interface ManageDashboardLayoutsDialogData {
styleUrls: ['./manage-dashboard-layouts-dialog.component.scss', '../../../components/dashboard/layout-button.scss'] styleUrls: ['./manage-dashboard-layouts-dialog.component.scss', '../../../components/dashboard/layout-button.scss']
}) })
export class ManageDashboardLayoutsDialogComponent extends DialogComponent<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts> export class ManageDashboardLayoutsDialogComponent extends DialogComponent<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>
implements ErrorStateMatcher { implements ErrorStateMatcher, OnDestroy {
@ViewChild('tooltip', {static: true}) tooltip: MatTooltip; @ViewChild('tooltip') tooltip: MatTooltip;
layoutsFormGroup: UntypedFormGroup; layoutsFormGroup: UntypedFormGroup;
@ -70,29 +93,43 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutFixedSize = LayoutFixedSize; layoutFixedSize = LayoutFixedSize;
layoutTypes = layoutTypes;
layoutTypeTranslations = layoutTypeTranslationMap;
layoutBreakpoints: DashboardLayoutSettings[] = [];
private readonly layouts: DashboardStateLayouts; private readonly layouts: DashboardStateLayouts;
private subscriptions: Array<Subscription> = []; private subscriptions: Array<Subscription> = [];
private submitted = false; private submitted = false;
allowBreakpointIds: BreakpointId[] = [];
selectedBreakpointIds: BreakpointId[] = ['default'];
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) private data: ManageDashboardLayoutsDialogData, @Inject(MAT_DIALOG_DATA) private data: ManageDashboardLayoutsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher, @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
protected dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>, protected dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService, private dashboardUtils: DashboardUtilsService,
private translate: TranslateService, private translate: TranslateService,
private dialog: MatDialog) { private dialog: MatDialog,
private dialogs: DialogService) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.layouts = this.data.layouts; this.layouts = this.data.layouts;
let layoutType = LayoutType.default;
if (isDefined(this.layouts.right)) {
layoutType = LayoutType.divider;
} else if (isDefined(this.layouts.main.gridSettings.layoutType)) {
layoutType = this.layouts.main.gridSettings.layoutType;
}
this.layoutsFormGroup = this.fb.group({ this.layoutsFormGroup = this.fb.group({
layoutType: [layoutType],
main: [{value: isDefined(this.layouts.main), disabled: true}], main: [{value: isDefined(this.layouts.main), disabled: true}],
right: [isDefined(this.layouts.right)],
sliderPercentage: [50], sliderPercentage: [50],
sliderFixed: [this.layoutFixedSize.MIN], sliderFixed: [this.layoutFixedSize.MIN],
leftWidthPercentage: [50, leftWidthPercentage: [50,
@ -141,7 +178,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
sliderFixed: this.layouts.main.gridSettings.layoutDimension.fixedWidth sliderFixed: this.layouts.main.gridSettings.layoutDimension.fixedWidth
}, {emitEvent: false}); }, {emitEvent: false});
} else { } else {
const leftWidthPercentage: number = Number(this.layouts.main.gridSettings.layoutDimension.leftWidthPercentage); const leftWidthPercentage = Number(this.layouts.main.gridSettings.layoutDimension.leftWidthPercentage);
this.layoutsFormGroup.patchValue({ this.layoutsFormGroup.patchValue({
leftWidthPercentage, leftWidthPercentage,
sliderPercentage: leftWidthPercentage, sliderPercentage: leftWidthPercentage,
@ -158,6 +195,21 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
this.layouts.right = this.dashboardUtils.createDefaultLayoutData(); this.layouts.right = this.dashboardUtils.createDefaultLayoutData();
} }
this.addLayoutConfiguration('default');
if (!this.isDividerLayout && this.layouts.main.breakpoints) {
for (const breakpoint of (Object.keys(this.layouts.main.breakpoints) as BreakpointId[])) {
this.addLayoutConfiguration(breakpoint);
this.selectedBreakpointIds.push(breakpoint);
}
}
this.allowBreakpointIds = Object.values(this.dashboardUtils.getListBreakpoint())
.filter((item) => !this.selectedBreakpointIds.includes(item.id))
.map(item => item.id);
this.sortLayoutBreakpoints();
this.subscriptions.push( this.subscriptions.push(
this.layoutsFormGroup.get('sliderPercentage').valueChanges this.layoutsFormGroup.get('sliderPercentage').valueChanges
.subscribe( .subscribe(
@ -209,8 +261,10 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
return originalErrorState || customErrorState; return originalErrorState || customErrorState;
} }
openLayoutSettings(layoutId: DashboardLayoutId) { openLayoutSettings(layoutId: DashboardLayoutId, breakpointId: BreakpointId = 'default') {
const gridSettings = deepClone(this.layouts[layoutId].gridSettings); const layout = this.dashboardUtils.getDashboardLayoutConfig(this.layouts[layoutId], breakpointId);
const gridSettings = layout.gridSettings;
gridSettings.layoutType = this.layoutsFormGroup.get('layoutType').value;
this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData, this.dialog.open<DashboardSettingsDialogComponent, DashboardSettingsDialogData,
DashboardSettingsDialogData>(DashboardSettingsDialogComponent, { DashboardSettingsDialogData>(DashboardSettingsDialogComponent, {
disableClose: true, disableClose: true,
@ -218,11 +272,12 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
data: { data: {
settings: null, settings: null,
gridSettings, gridSettings,
isRightLayout: this.layoutsFormGroup.get('right').value && layoutId === 'right' isRightLayout: this.isDividerLayout && layoutId === 'right',
breakpointId
} }
}).afterClosed().subscribe((data) => { }).afterClosed().subscribe((data) => {
if (data && data.gridSettings) { if (data && data.gridSettings) {
this.dashboardUtils.updateLayoutSettings(this.layouts[layoutId], data.gridSettings); this.dashboardUtils.updateLayoutSettings(layout, data.gridSettings);
this.layoutsFormGroup.markAsDirty(); this.layoutsFormGroup.markAsDirty();
} }
}); });
@ -234,20 +289,24 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
save(): void { save(): void {
this.submitted = true; this.submitted = true;
const layouts = ['main', 'right']; const layoutType = this.layoutsFormGroup.value.layoutType;
for (const l of layouts) { this.layouts.main.gridSettings.layoutType = layoutType;
const control = this.layoutsFormGroup.controls[l]; if (!this.isDividerLayout) {
if (!control.value) { delete this.layouts.right;
if (this.layouts[l]) { if (this.layouts.main.breakpoints) {
delete this.layouts[l]; for (const breakpoint of Object.values(this.layouts.main.breakpoints)) {
breakpoint.gridSettings.layoutType = layoutType;
} }
} }
} else {
delete this.layouts.main.breakpoints;
this.layouts.right.gridSettings.layoutType = layoutType;
} }
delete this.layouts.main.gridSettings.layoutDimension; delete this.layouts.main.gridSettings.layoutDimension;
if (this.layouts.right?.gridSettings) { if (this.layouts.right?.gridSettings) {
delete this.layouts.right.gridSettings.layoutDimension; delete this.layouts.right.gridSettings.layoutDimension;
} }
if (this.layoutsFormGroup.value.right) { if (this.isDividerLayout) {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
const widthType = formValues.type; const widthType = formValues.type;
const layoutDimension: LayoutDimension = { const layoutDimension: LayoutDimension = {
@ -271,7 +330,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
buttonFlexValue(): number { buttonFlexValue(): number {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
if (formValues.right) { if (this.isDividerLayout) {
if (formValues.type !== LayoutWidthType.FIXED) { if (formValues.type !== LayoutWidthType.FIXED) {
return formValues.leftWidthPercentage; return formValues.leftWidthPercentage;
} else { } else {
@ -288,7 +347,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
return this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED ? value : `${value}|${100 - value}`; return this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED ? value : `${value}|${100 - value}`;
} }
private layoutControlChange(key: string, value) { private layoutControlChange(key: string, value: number) {
const valueToSet = 100 - Number(value); const valueToSet = 100 - Number(value);
this.layoutsFormGroup.get(key).setValue(valueToSet, {emitEvent: false}); this.layoutsFormGroup.get(key).setValue(valueToSet, {emitEvent: false});
this.layoutsFormGroup.get('sliderPercentage') this.layoutsFormGroup.get('sliderPercentage')
@ -296,7 +355,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
} }
setFixedLayout(layout: string): void { setFixedLayout(layout: string): void {
if (this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED && this.layoutsFormGroup.get('right').value) { if (this.layoutsFormGroup.get('type').value === LayoutWidthType.FIXED && this.isDividerLayout) {
this.layoutsFormGroup.get('fixedLayout').setValue(layout); this.layoutsFormGroup.get('fixedLayout').setValue(layout);
this.layoutsFormGroup.get('fixedLayout').markAsDirty(); this.layoutsFormGroup.get('fixedLayout').markAsDirty();
} }
@ -339,7 +398,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutButtonClass(side: DashboardLayoutId, border: boolean = false): string { layoutButtonClass(side: DashboardLayoutId, border: boolean = false): string {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
if (formValues.right) { if (this.isDividerLayout) {
let classString = border ? 'tb-layout-button-main ' : ''; let classString = border ? 'tb-layout-button-main ' : '';
if (!(formValues.fixedLayout === side || formValues.type === LayoutWidthType.PERCENTAGE)) { if (!(formValues.fixedLayout === side || formValues.type === LayoutWidthType.PERCENTAGE)) {
classString += 'tb-fixed-layout-button'; classString += 'tb-fixed-layout-button';
@ -350,7 +409,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutButtonText(side: DashboardLayoutId): string { layoutButtonText(side: DashboardLayoutId): string {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
if (!(formValues.fixedLayout === side || !formValues.right || formValues.type === LayoutWidthType.PERCENTAGE)) { if (!(formValues.fixedLayout === side || !this.isDividerLayout || formValues.type === LayoutWidthType.PERCENTAGE)) {
if (side === 'main') { if (side === 'main') {
return this.translate.instant('layout.left-side'); return this.translate.instant('layout.left-side');
} else { } else {
@ -361,6 +420,116 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
showPreviewInputs(side: DashboardLayoutId): boolean { showPreviewInputs(side: DashboardLayoutId): boolean {
const formValues = this.layoutsFormGroup.value; const formValues = this.layoutsFormGroup.value;
return formValues.right && (formValues.type === LayoutWidthType.PERCENTAGE || formValues.fixedLayout === side); return this.isDividerLayout && (formValues.type === LayoutWidthType.PERCENTAGE || formValues.fixedLayout === side);
}
get isDividerLayout(): boolean {
return this.layoutsFormGroup.get('layoutType').value === LayoutType.divider;
}
addBreakpoint() {
this.dialog.open<AddNewBreakpointDialogComponent, AddNewBreakpointDialogData,
AddNewBreakpointDialogResult>(AddNewBreakpointDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
allowBreakpointIds: this.allowBreakpointIds,
selectedBreakpointIds: this.selectedBreakpointIds
}
}).afterClosed().subscribe((data) => {
if (data) {
this.createdNewBreakpoint(data.newBreakpointId, data.copyFrom);
this.layoutsFormGroup.markAsDirty();
}
});
}
deleteBreakpoint($event: Event, breakpointId: BreakpointId): void {
if ($event) {
$event.stopPropagation();
}
const title = this.translate.instant('layout.delete-breakpoint-title', {name: breakpointId});
const content = this.translate.instant('layout.delete-breakpoint-text');
this.dialogs.confirm(title, content, this.translate.instant('action.no'),
this.translate.instant('action.yes')).subscribe((res) => {
if (res) {
delete this.layouts.main.breakpoints[breakpointId];
if (isEqual(this.layouts.main.breakpoints, {})) {
delete this.layouts.main.breakpoints;
}
this.layoutBreakpoints = this.layoutBreakpoints.filter((item) => item.breakpoint !== breakpointId);
this.allowBreakpointIds.push(breakpointId);
this.selectedBreakpointIds = this.selectedBreakpointIds.filter((item) => item !== breakpointId);
this.sortLayoutBreakpoints();
this.layoutsFormGroup.markAsDirty();
}
}
);
}
private createdNewBreakpoint(newBreakpointId: BreakpointId, copyFromBreakpointId: BreakpointId): void {
const layoutConfig = this.layouts.main;
const sourceLayout = copyFromBreakpointId === 'default' ? layoutConfig : layoutConfig.breakpoints[copyFromBreakpointId];
const gridSettings = deepClone(sourceLayout.gridSettings);
const widgets = deepClone(sourceLayout.widgets);
if (copyFromBreakpointId === 'default') {
const breakpointInfo = this.dashboardUtils.getBreakpointInfoById(newBreakpointId);
if (breakpointInfo?.maxWidth < 960) {
gridSettings.viewFormat = ViewFormatType.list;
gridSettings.rowHeight = gridSettings.mobileRowHeight;
gridSettings.autoFillHeight = gridSettings.mobileAutoFillHeight;
}
for (const widgetId in widgets) {
if (widgets[widgetId]) {
delete widgets[widgetId].desktopHide;
delete widgets[widgetId].mobileHide;
}
}
}
if (!layoutConfig.breakpoints) {
layoutConfig.breakpoints = {};
}
layoutConfig.breakpoints[newBreakpointId] = {
gridSettings,
widgets,
};
this.selectedBreakpointIds.push(newBreakpointId);
this.allowBreakpointIds = this.allowBreakpointIds.filter((item) => item !== newBreakpointId);
this.addLayoutConfiguration(newBreakpointId);
this.sortLayoutBreakpoints();
}
private addLayoutConfiguration(breakpointId: BreakpointId) {
const layout = breakpointId === 'default' ? this.layouts.main : this.layouts.main.breakpoints[breakpointId];
const size = breakpointId === 'default' ? '' : this.dashboardUtils.getBreakpointSizeDescription(breakpointId);
this.layoutBreakpoints.push({
icon: this.dashboardUtils.getBreakpointIcon(breakpointId),
name: this.dashboardUtils.getBreakpointName(breakpointId),
layout,
descriptionSize: size,
breakpoint: breakpointId
});
}
private sortLayoutBreakpoints() {
this.layoutBreakpoints.sort((a, b) => {
const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a.breakpoint)?.maxWidth || Infinity;
const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b.breakpoint)?.maxWidth || Infinity;
return bMaxWidth - aMaxWidth;
});
this.selectedBreakpointIds.sort((a, b) => {
const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity;
const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity;
return bMaxWidth - aMaxWidth;
});
this.allowBreakpointIds.sort((a, b) => {
const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity;
const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity;
return bMaxWidth - aMaxWidth;
});
} }
} }

29
ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html

@ -0,0 +1,29 @@
<!--
Copyright © 2016-2024 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-select
[class.hidden]="breakpointIds.length < 2"
class="select-dashboard-breakpoint"
[(ngModel)]="selectedBreakpoint"
(ngModelChange)="selectLayoutChanged()">
<mat-select-trigger>{{ getName(selectedBreakpoint) }}</mat-select-trigger>
<mat-option *ngFor="let breakpointId of breakpointIds" [value]="breakpointId">
<mat-icon><tb-icon>{{ getIcon(breakpointId) }}</tb-icon></mat-icon>
<div>{{ getName(breakpointId) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpointId) }}</div>
</mat-option>
</mat-select>

34
ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.scss

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2024 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 {
pointer-events: all;
width: min-content; //for Safari
.hidden {
display: none;
}
}
:host ::ng-deep {
.mat-mdc-select.select-dashboard-breakpoint {
.mat-mdc-select-value {
max-width: 200px;
}
.mat-mdc-select-arrow {
width: 24px;
}
}
}

81
ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.ts

@ -0,0 +1,81 @@
///
/// Copyright © 2016-2024 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, Input, OnDestroy, OnInit } from '@angular/core';
import { DashboardPageComponent } from '@home/components/dashboard-page/dashboard-page.component';
import { Subscription } from 'rxjs';
import { BreakpointId } from '@shared/models/dashboard.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
@Component({
selector: 'tb-select-dashboard-breakpoint',
templateUrl: './select-dashboard-breakpoint.component.html',
styleUrls: ['./select-dashboard-breakpoint.component.scss']
})
export class SelectDashboardBreakpointComponent implements OnInit, OnDestroy {
@Input()
dashboardCtrl: DashboardPageComponent;
selectedBreakpoint: BreakpointId = 'default';
breakpointIds: Array<BreakpointId> = ['default'];
private layoutDataChanged$: Subscription;
constructor(private dashboardUtils: DashboardUtilsService) {
}
ngOnInit() {
this.layoutDataChanged$ = this.dashboardCtrl.layouts.main.layoutCtx.layoutDataChanged.subscribe(() => {
if (this.dashboardCtrl.layouts.main.layoutCtx.layoutData) {
this.breakpointIds = Object.keys(this.dashboardCtrl.layouts.main.layoutCtx?.layoutData) as BreakpointId[];
this.breakpointIds.sort((a, b) => {
const aMaxWidth = this.dashboardUtils.getBreakpointInfoById(a)?.maxWidth || Infinity;
const bMaxWidth = this.dashboardUtils.getBreakpointInfoById(b)?.maxWidth || Infinity;
return bMaxWidth - aMaxWidth;
});
if (this.breakpointIds.indexOf(this.dashboardCtrl.layouts.main.layoutCtx.breakpoint) > -1) {
this.selectedBreakpoint = this.dashboardCtrl.layouts.main.layoutCtx.breakpoint;
} else {
this.selectedBreakpoint = 'default';
this.dashboardCtrl.layouts.main.layoutCtx.breakpoint = this.selectedBreakpoint;
}
}
});
}
ngOnDestroy() {
this.layoutDataChanged$.unsubscribe();
}
selectLayoutChanged() {
this.dashboardCtrl.layouts.main.layoutCtx.ctrl.updatedCurrentBreakpoint(this.selectedBreakpoint);
this.dashboardCtrl.updateLayoutSizes();
}
getName(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointName(breakpointId);
}
getIcon(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointIcon(breakpointId);
}
getSizeDescription(breakpointId: BreakpointId): string {
return this.dashboardUtils.getBreakpointSizeDescription(breakpointId);
}
}

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

@ -58,7 +58,7 @@
[disabled]="!item.enabled" [disabled]="!item.enabled"
(click)="item.action(widgetContextMenuEvent, widget)"> (click)="item.action(widgetContextMenuEvent, widget)">
<span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span> <span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon> <tb-icon matMenuItemIcon *ngIf="item.icon">{{item.icon}}</tb-icon>
<span translate>{{item.value}}</span> <span translate>{{item.value}}</span>
</button> </button>
</div> </div>

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

@ -17,7 +17,6 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
DoCheck, DoCheck,
Input, Input,
@ -46,7 +45,6 @@ import {
} from '../../models/dashboard-component.models'; } from '../../models/dashboard-component.models';
import { ReplaySubject, Subject, Subscription } from 'rxjs'; import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models'; import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils'; import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants'; import { MediaBreakpoints } from '@shared/models/constants';
@ -203,9 +201,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widgetContextMenuEvent: MouseEvent; widgetContextMenuEvent: MouseEvent;
dashboardWidgets = new DashboardWidgets(this, dashboardWidgets = new DashboardWidgets(this,
this.differs.find([]).create<Widget>((index, item) => { this.differs.find([]).create<Widget>((_, item) => item),
return item;
}),
this.kvDiffers.find([]).create<string, WidgetLayout>() this.kvDiffers.find([]).create<string, WidgetLayout>()
); );
@ -218,11 +214,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
public utils: UtilsService, public utils: UtilsService,
private timeService: TimeService, private timeService: TimeService,
private dialogService: DialogService,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
private differs: IterableDiffers, private differs: IterableDiffers,
private kvDiffers: KeyValueDiffers, private kvDiffers: KeyValueDiffers,
private cd: ChangeDetectorRef,
private ngZone: NgZone) { private ngZone: NgZone) {
super(store); super(store);
this.authUser = getCurrentAuthUser(store); this.authUser = getCurrentAuthUser(store);
@ -257,8 +251,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
displayGrid: this.displayGrid, displayGrid: this.displayGrid,
resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50}, resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50},
draggable: {enabled: this.isEdit && !this.isEditingWidget}, draggable: {enabled: this.isEdit && !this.isEditingWidget},
itemChangeCallback: item => this.dashboardWidgets.sortWidgets(), itemChangeCallback: () => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (item, itemComponent) => { itemInitCallback: (_, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent; (itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
}, },
colWidthUpdateCallback: (colWidth) => { colWidthUpdateCallback: (colWidth) => {
@ -421,7 +415,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) { private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) {
if (this.callbacks && this.callbacks.prepareWidgetContextMenu) { if (this.callbacks && this.callbacks.prepareWidgetContextMenu) {
const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget); const items = this.callbacks.prepareWidgetContextMenu($event, widget.widget, widget.isReference);
if (items && items.length) { if (items && items.length) {
$event.preventDefault(); $event.preventDefault();
$event.stopPropagation(); $event.stopPropagation();
@ -459,6 +453,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
case WidgetComponentActionType.REMOVE: case WidgetComponentActionType.REMOVE:
this.removeWidget($event, widget); this.removeWidget($event, widget);
break; break;
case WidgetComponentActionType.REPLACE_REFERENCE_WITH_WIDGET_COPY:
this.replaceReferenceWithWidgetCopy($event, widget);
break;
} }
} }
@ -483,6 +480,15 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} }
} }
private replaceReferenceWithWidgetCopy($event: Event, widget: DashboardWidget) {
if ($event) {
$event.stopPropagation();
}
if (this.isEditActionEnabled && this.callbacks && this.callbacks.replaceReferenceWithWidgetCopy) {
this.callbacks.replaceReferenceWithWidgetCopy($event, widget.widget);
}
}
private exportWidget($event: Event, widget: DashboardWidget) { private exportWidget($event: Event, widget: DashboardWidget) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
@ -558,7 +564,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widget.gridsterItemComponent$().subscribe((gridsterItem) => { widget.gridsterItemComponent$().subscribe((gridsterItem) => {
const gridsterItemElement = gridsterItem.el as HTMLElement; const gridsterItemElement = gridsterItem.el as HTMLElement;
const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2; const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2;
let scrollTop; let scrollTop: number;
if (this.isMobileSize) { if (this.isMobileSize) {
scrollTop = gridsterItemElement.offsetTop; scrollTop = gridsterItemElement.offsetTop;
} else { } else {
@ -592,8 +598,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} else { } else {
this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : this.gridType || GridType.ScrollVertical; this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : this.gridType || GridType.ScrollVertical;
} }
const mobileBreakPoint = this.isMobileSize ? 20000 : 0; this.gridsterOpts.mobileBreakpoint = this.isMobileSize ? 20000 : 0;
this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;
const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight); const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight);
if (this.gridsterOpts.fixedRowHeight !== rowSize) { if (this.gridsterOpts.fixedRowHeight !== rowSize) {
this.gridsterOpts.fixedRowHeight = rowSize; this.gridsterOpts.fixedRowHeight = rowSize;

6
ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts

@ -402,7 +402,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
this.cd.detectChanges(); this.cd.detectChanges();
} }
updateData(closeDetails: boolean = true) { updateData(closeDetails: boolean = true, reloadEntity: boolean = true) {
if (closeDetails) { if (closeDetails) {
this.isDetailsOpen = false; this.isDetailsOpen = false;
} }
@ -427,7 +427,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
timePageLink.endTime = interval.endTime; timePageLink.endTime = interval.endTime;
} }
this.dataSource.loadEntities(this.pageLink); this.dataSource.loadEntities(this.pageLink);
if (this.isDetailsOpen && this.entityDetailsPanel) { if (reloadEntity && this.isDetailsOpen && this.entityDetailsPanel) {
this.entityDetailsPanel.reloadEntity(); this.entityDetailsPanel.reloadEntity();
} }
} }
@ -511,7 +511,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} }
onEntityUpdated(entity: BaseData<HasId>) { onEntityUpdated(entity: BaseData<HasId>) {
this.updateData(false); this.updateData(false, false);
this.entitiesTableConfig.entityUpdated(entity); this.entitiesTableConfig.entityUpdated(entity);
} }

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

@ -40,11 +40,12 @@ import { UntypedFormGroup } from '@angular/forms';
import { EntityComponent } from './entity.component'; import { EntityComponent } from './entity.component';
import { TbAnchorComponent } from '@shared/components/tb-anchor.component'; import { TbAnchorComponent } from '@shared/components/tb-anchor.component';
import { EntityAction } from '@home/models/entity/entity-component.models'; import { EntityAction } from '@home/models/entity/entity-component.models';
import { Observable, ReplaySubject, Subscription } from 'rxjs'; import { Observable, ReplaySubject, Subscription, throwError } from 'rxjs';
import { MatTab, MatTabGroup } from '@angular/material/tabs'; import { MatTab, MatTabGroup } from '@angular/material/tabs';
import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component'; import { EntityTabsComponent } from '@home/components/entity/entity-tabs.component';
import { deepClone, mergeDeep } from '@core/utils'; import { deepClone, mergeDeep } from '@core/utils';
import { entityIdEquals } from '@shared/models/id/entity-id'; import { catchError } from 'rxjs/operators';
import { HttpStatusCode } from '@angular/common/http';
@Component({ @Component({
selector: 'tb-entity-details-panel', selector: 'tb-entity-details-panel',
@ -288,7 +289,16 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV
editingEntity.additionalInfo = editingEntity.additionalInfo =
mergeDeep((this.editingEntity as any).additionalInfo, this.entityComponent.entityFormValue()?.additionalInfo); mergeDeep((this.editingEntity as any).additionalInfo, this.entityComponent.entityFormValue()?.additionalInfo);
} }
this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity).subscribe( this.entitiesTableConfig.saveEntity(editingEntity, this.editingEntity)
.pipe(
catchError((err) => {
if (err.status === HttpStatusCode.Conflict) {
return this.entitiesTableConfig.loadEntity(this.currentEntityId);
}
return throwError(() => err);
})
)
.subscribe(
(entity) => { (entity) => {
this.entity = entity; this.entity = entity;
this.entityComponent.entity = entity; this.entityComponent.entity = entity;

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

@ -114,6 +114,9 @@ import { EditWidgetComponent } from '@home/components/dashboard-page/edit-widget
import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component'; import { DashboardWidgetSelectComponent } from '@home/components/dashboard-page/dashboard-widget-select.component';
import { AddWidgetDialogComponent } from '@home/components/dashboard-page/add-widget-dialog.component'; import { AddWidgetDialogComponent } from '@home/components/dashboard-page/add-widget-dialog.component';
import { ManageDashboardLayoutsDialogComponent } from '@home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component'; import { ManageDashboardLayoutsDialogComponent } from '@home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component';
import {
AddNewBreakpointDialogComponent
} from '@home/components/dashboard-page/layout/add-new-breakpoint-dialog.component';
import { DashboardSettingsDialogComponent } from '@home/components/dashboard-page/dashboard-settings-dialog.component'; import { DashboardSettingsDialogComponent } from '@home/components/dashboard-page/dashboard-settings-dialog.component';
import { ManageDashboardStatesDialogComponent } from '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component'; import { ManageDashboardStatesDialogComponent } from '@home/components/dashboard-page/states/manage-dashboard-states-dialog.component';
import { DashboardStateDialogComponent } from '@home/components/dashboard-page/states/dashboard-state-dialog.component'; import { DashboardStateDialogComponent } from '@home/components/dashboard-page/states/dashboard-state-dialog.component';
@ -175,6 +178,9 @@ import { WidgetConfigComponentsModule } from '@home/components/widget/config/wid
import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module'; import { BasicWidgetConfigModule } from '@home/components/widget/config/basic/basic-widget-config.module';
import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component'; import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.component';
import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layout/move-widgets-dialog.component'; import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layout/move-widgets-dialog.component';
import {
SelectDashboardBreakpointComponent
} from '@home/components/dashboard-page/layout/select-dashboard-breakpoint.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -285,11 +291,13 @@ import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layo
DashboardPageComponent, DashboardPageComponent,
DashboardStateComponent, DashboardStateComponent,
DashboardLayoutComponent, DashboardLayoutComponent,
SelectDashboardBreakpointComponent,
EditWidgetComponent, EditWidgetComponent,
DashboardWidgetSelectComponent, DashboardWidgetSelectComponent,
AddWidgetDialogComponent, AddWidgetDialogComponent,
MoveWidgetsDialogComponent, MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent, ManageDashboardLayoutsDialogComponent,
AddNewBreakpointDialogComponent,
DashboardSettingsDialogComponent, DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent, DashboardStateDialogComponent,
@ -418,11 +426,13 @@ import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layo
DashboardPageComponent, DashboardPageComponent,
DashboardStateComponent, DashboardStateComponent,
DashboardLayoutComponent, DashboardLayoutComponent,
SelectDashboardBreakpointComponent,
EditWidgetComponent, EditWidgetComponent,
DashboardWidgetSelectComponent, DashboardWidgetSelectComponent,
AddWidgetDialogComponent, AddWidgetDialogComponent,
MoveWidgetsDialogComponent, MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent, ManageDashboardLayoutsDialogComponent,
AddNewBreakpointDialogComponent,
DashboardSettingsDialogComponent, DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent, ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent, DashboardStateDialogComponent,

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

@ -107,7 +107,7 @@ export class RouterTabsComponent extends PageComponent implements OnInit {
if (found) { if (found) {
const rootPath = sectionPath.substring(0, sectionPath.length - found.path.length); const rootPath = sectionPath.substring(0, sectionPath.length - found.path.length);
const isRoot = rootPath === ''; const isRoot = rootPath === '';
const tabs: Array<MenuSection> = found ? found.pages.filter(page => !page.disabled && (!page.rootOnly || isRoot)) : []; const tabs: Array<MenuSection> = found ? found.pages.filter(page => !page.rootOnly || isRoot) : [];
return tabs.map((tab) => ({...tab, path: rootPath + tab.path})); return tabs.map((tab) => ({...tab, path: rootPath + tab.path}));
} }
return []; return [];

3
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.ts

@ -57,7 +57,6 @@ import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { SharedModule } from '@shared/shared.module'; import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({ @Component({
@ -78,7 +77,7 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs
} }
], ],
standalone: true, standalone: true,
imports: [CommonModule, SharedModule, TruncateWithTooltipDirective] imports: [CommonModule, SharedModule]
}) })
export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { export class MappingTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts

@ -41,7 +41,6 @@ import { generateSecret } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-modbus-data-keys-panel', selector: 'tb-modbus-data-keys-panel',
@ -52,7 +51,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
CommonModule, CommonModule,
SharedModule, SharedModule,
GatewayHelpLinkPipe, GatewayHelpLinkPipe,
TruncateWithTooltipDirective,
] ]
}) })
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-security-config/modbus-security-config.component.ts

@ -42,7 +42,6 @@ import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-modbus-security-config', selector: 'tb-modbus-security-config',
@ -64,7 +63,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
TruncateWithTooltipDirective,
] ]
}) })
export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy { export class ModbusSecurityConfigComponent implements ControlValueAccessor, Validator, OnChanges, OnDestroy {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts

@ -48,7 +48,6 @@ import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipe
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component';
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component';
import { isEqual } from '@core/utils'; import { isEqual } from '@core/utils';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-modbus-slave-config', selector: 'tb-modbus-slave-config',
@ -73,7 +72,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
ModbusValuesComponent, ModbusValuesComponent,
ModbusSecurityConfigComponent, ModbusSecurityConfigComponent,
GatewayPortTooltipPipe, GatewayPortTooltipPipe,
TruncateWithTooltipDirective,
], ],
}) })
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts

@ -52,7 +52,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils'; import { isEqual } from '@core/utils';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { helpBaseUrl } from '@shared/models/constants'; import { helpBaseUrl } from '@shared/models/constants';
@Component({ @Component({
@ -78,7 +77,6 @@ import { helpBaseUrl } from '@shared/models/constants';
ModbusValuesComponent, ModbusValuesComponent,
ModbusSecurityConfigComponent, ModbusSecurityConfigComponent,
GatewayPortTooltipPipe, GatewayPortTooltipPipe,
TruncateWithTooltipDirective,
], ],
styleUrls: ['./modbus-slave-dialog.component.scss'], styleUrls: ['./modbus-slave-dialog.component.scss'],
}) })

21
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.html

@ -84,6 +84,27 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.poll-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.poll-period' | translate }}</div>
</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="50" name="value"
formControlName="pollPeriodInMillis" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="'gateway.poll-period-error' | translate: {min: 50}"
*ngIf="(serverConfigFormGroup.get('pollPeriodInMillis').hasError('required') ||
serverConfigFormGroup.get('pollPeriodInMillis').hasError('min')) &&
serverConfigFormGroup.get('pollPeriodInMillis').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}"> <div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}">
<div tbTruncateWithTooltip>{{ 'gateway.sub-check-period' | translate }}</div> <div tbTruncateWithTooltip>{{ 'gateway.sub-check-period' | translate }}</div>

10
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts

@ -35,10 +35,10 @@ import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { import {
SecurityConfigComponent SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import { HOUR } from '@shared/models/time/time.models';
@Component({ @Component({
selector: 'tb-opc-server-config', selector: 'tb-opc-server-config',
@ -62,7 +62,6 @@ import {
CommonModule, CommonModule,
SharedModule, SharedModule,
SecurityConfigComponent, SecurityConfigComponent,
TruncateWithTooltipDirective,
] ]
}) })
export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy { export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@ -79,7 +78,8 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
this.serverConfigFormGroup = this.fb.group({ this.serverConfigFormGroup = this.fb.group({
url: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], url: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
timeoutInMillis: [1000, [Validators.required, Validators.min(1000)]], timeoutInMillis: [1000, [Validators.required, Validators.min(1000)]],
scanPeriodInMillis: [1000, [Validators.required, Validators.min(1000)]], scanPeriodInMillis: [HOUR, [Validators.required, Validators.min(1000)]],
pollPeriodInMillis: [5000, [Validators.required, Validators.min(50)]],
enableSubscriptions: [true, []], enableSubscriptions: [true, []],
subCheckPeriodInMillis: [100, [Validators.required, Validators.min(100)]], subCheckPeriodInMillis: [100, [Validators.required, Validators.min(100)]],
showMap: [false, []], showMap: [false, []],
@ -117,7 +117,8 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
writeValue(serverConfig: ServerConfig): void { writeValue(serverConfig: ServerConfig): void {
const { const {
timeoutInMillis = 1000, timeoutInMillis = 1000,
scanPeriodInMillis = 1000, scanPeriodInMillis = HOUR,
pollPeriodInMillis = 5000,
enableSubscriptions = true, enableSubscriptions = true,
subCheckPeriodInMillis = 100, subCheckPeriodInMillis = 100,
showMap = false, showMap = false,
@ -129,6 +130,7 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
...serverConfig, ...serverConfig,
timeoutInMillis, timeoutInMillis,
scanPeriodInMillis, scanPeriodInMillis,
pollPeriodInMillis,
enableSubscriptions, enableSubscriptions,
subCheckPeriodInMillis, subCheckPeriodInMillis,
showMap, showMap,

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/workers-config-control/workers-config-control.component.ts

@ -35,7 +35,6 @@ import { CommonModule } from '@angular/common';
import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { WorkersConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-workers-config-control', selector: 'tb-workers-config-control',
@ -45,7 +44,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
TruncateWithTooltipDirective,
], ],
providers: [ providers: [
{ {

78
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.html

@ -726,6 +726,10 @@
<mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('min')"> <mat-error *ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('min')">
{{ 'gateway.min-pack-send-delay-min' | translate }} {{ 'gateway.min-pack-send-delay-min' | translate }}
</mat-error> </mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.minPackSendDelayMS').hasError('pattern')">
{{ 'gateway.min-pack-send-delay-min-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;" <mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.minimal-pack-delay' | translate }}">info_outlined matTooltip="{{ 'gateway.hints.minimal-pack-delay' | translate }}">info_outlined
</mat-icon> </mat-icon>
@ -747,22 +751,64 @@
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
</section> </section>
<mat-form-field appearance="outline" class="mat-block"> <section class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-label translate>gateway.statistics.check-connectors-configuration</mat-label> <mat-form-field appearance="outline" class="flex">
<input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number" min="0"/> <mat-label translate>gateway.statistics.check-connectors-configuration</mat-label>
<mat-error <input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number" min="0"/>
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')"> <mat-error
{{ 'gateway.statistics.check-connectors-configuration-required' | translate }} *ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')">
</mat-error> {{ 'gateway.statistics.check-connectors-configuration-required' | translate }}
<mat-error </mat-error>
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')"> <mat-error
{{ 'gateway.statistics.check-connectors-configuration-min' | translate }} *ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')">
</mat-error> {{ 'gateway.statistics.check-connectors-configuration-min' | translate }}
<mat-error </mat-error>
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')"> <mat-error
{{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }} *ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')">
</mat-error> {{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }}
</mat-form-field> </mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.statistics.max-payload-size-bytes</mat-label>
<input matInput formControlName="maxPayloadSizeBytes" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.maxPayloadSizeBytes').hasError('required')">
{{ 'gateway.statistics.max-payload-size-bytes-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.maxPayloadSizeBytes').hasError('min')">
{{ 'gateway.statistics.max-payload-size-bytes-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.maxPayloadSizeBytes').hasError('pattern')">
{{ 'gateway.statistics.max-payload-size-bytes-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.max-payload-size-bytes' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
<section class="tb-form-row no-border no-padding tb-standard-fields column-xs">
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.statistics.min-pack-size-to-send</mat-label>
<input matInput formControlName="minPackSizeToSend" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.minPackSizeToSend').hasError('required')">
{{ 'gateway.statistics.min-pack-size-to-send-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.minPackSizeToSend').hasError('min')">
{{ 'gateway.statistics.min-pack-size-to-send-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.minPackSizeToSend').hasError('pattern')">
{{ 'gateway.statistics.min-pack-size-to-send-pattern' | translate }}
</mat-error>
<mat-icon matIconSuffix style="cursor:pointer;"
matTooltip="{{ 'gateway.hints.min-pack-size-to-send' | translate }}">info_outlined
</mat-icon>
</mat-form-field>
</section>
</div> </div>
</div> </div>
</ng-template> </ng-template>

6
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-configuration.component.ts

@ -102,9 +102,9 @@ export class GatewayConfigurationComponent implements OnInit {
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(60), Validators.pattern(/^-?[0-9]+$/)]], statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(60), Validators.pattern(/^-?[0-9]+$/)]],
commands: this.fb.array([], []) commands: this.fb.array([], [])
}), }),
maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], maxPayloadSizeBytes: [8196, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [200, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]], minPackSendDelayMS: [50, [Validators.required, Validators.min(10), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]], minPackSizeToSend: [500, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [true, []], handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({ checkingDeviceActivity: this.fb.group({
checkDeviceInactivity: [false, []], checkDeviceInactivity: [false, []],

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -151,6 +151,7 @@ export interface ServerConfig {
url: string; url: string;
timeoutInMillis: number; timeoutInMillis: number;
scanPeriodInMillis: number; scanPeriodInMillis: number;
pollPeriodInMillis: number;
enableSubscriptions: boolean; enableSubscriptions: boolean;
subCheckPeriodInMillis: number; subCheckPeriodInMillis: number;
showMap: boolean; showMap: boolean;

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

@ -49,6 +49,7 @@ import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order'; import { Direction } from '@shared/models/page/sort-order';
import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
@Component({ @Component({
selector: 'tb-quick-link', selector: 'tb-quick-link',
@ -114,6 +115,7 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private menuService: MenuService, private menuService: MenuService,
public translate: TranslateService, public translate: TranslateService,
private customTranslate: CustomTranslatePipe,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) { @SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
super(store); super(store);
} }
@ -135,7 +137,8 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control
this.updateView(modelValue); this.updateView(modelValue);
}), }),
map(value => value ? (typeof value === 'string' ? value : map(value => value ? (typeof value === 'string' ? value :
((value as any).translated ? value.name : this.translate.instant(value.name))) : ''), ((value as any).translated ? value.name
: value.customTranslate ? this.customTranslate.transform(value.name) : this.translate.instant(value.name))) : ''),
distinctUntilChanged(), distinctUntilChanged(),
switchMap(name => this.fetchLinks(name) ), switchMap(name => this.fetchLinks(name) ),
share() share()
@ -202,7 +205,8 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control
} }
displayLinkFn = (link?: MenuSection): string | undefined => displayLinkFn = (link?: MenuSection): string | undefined =>
link ? ((link as any).translated ? link.name : this.translate.instant(link.fullName || link.name)) : undefined; link ? ((link as any).translated ? link.name : link.customTranslate ? this.customTranslate.transform(link.fullName || link.name)
: this.translate.instant(link.fullName || link.name)) : undefined;
fetchLinks(searchText?: string): Observable<Array<MenuSection>> { fetchLinks(searchText?: string): Observable<Array<MenuSection>> {
this.searchText = searchText; this.searchText = searchText;
@ -228,7 +232,8 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control
map((links) => { map((links) => {
const result = deepClone(links); const result = deepClone(links);
for (const link of result) { for (const link of result) {
link.name = this.translate.instant(link.fullName || link.name); link.name = link.customTranslate ? this.customTranslate.transform(link.fullName || link.name)
: this.translate.instant(link.fullName || link.name);
(link as any).translated = true; (link as any).translated = true;
} }
return result; return result;

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

@ -26,7 +26,7 @@
<mat-grid-tile *ngFor="let place of sectionPlaces(section)"> <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)"> <a mat-raised-button color="primary" class="tb-card-button" href="{{place.path}}" (click)="navigate($event, place.path)">
<tb-icon matButtonIcon class="tb-mat-96">{{place.icon}}</tb-icon> <tb-icon matButtonIcon class="tb-mat-96">{{place.icon}}</tb-icon>
<span translate>{{place.name}}</span> <span>{{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}}</span>
</a> </a>
</mat-grid-tile> </mat-grid-tile>
</mat-grid-list> </mat-grid-list>

10
ui-ngx/src/app/modules/home/components/widget/lib/navigation-cards-widget.component.ts

@ -20,7 +20,7 @@ import { WidgetContext } from '@home/models/widget-component.models';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { MenuService } from '@core/services/menu.service'; import { MenuService } from '@core/services/menu.service';
import { HomeSection, HomeSectionPlace } from '@core/services/menu.models'; import { HomeSection, MenuSection } from '@core/services/menu.models';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -38,9 +38,7 @@ export class NavigationCardsWidgetComponent extends PageComponent implements OnI
homeSections$ = this.menuService.homeSections(); homeSections$ = this.menuService.homeSections();
showHomeSections$ = this.homeSections$.pipe( showHomeSections$ = this.homeSections$.pipe(
map((sections) => { map((sections) => sections.filter((section) => this.sectionPlaces(section).length > 0))
return sections.filter((section) => this.sectionPlaces(section).length > 0);
})
); );
cols = null; cols = null;
@ -85,11 +83,11 @@ export class NavigationCardsWidgetComponent extends PageComponent implements OnI
}); });
} }
sectionPlaces(section: HomeSection): HomeSectionPlace[] { sectionPlaces(section: HomeSection): MenuSection[] {
return section && section.places ? section.places.filter((place) => this.filterPlace(place)) : []; return section && section.places ? section.places.filter((place) => this.filterPlace(place)) : [];
} }
private filterPlace(place: HomeSectionPlace): boolean { private filterPlace(place: MenuSection): boolean {
if (this.settings.filterType === 'include') { if (this.settings.filterType === 'include') {
return this.settings.filter.includes(place.path); return this.settings.filter.includes(place.path);
} else if (this.settings.filterType === 'exclude') { } else if (this.settings.filterType === 'exclude') {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/scada/scada-symbol-object-settings.component.html

@ -40,7 +40,7 @@
<div class="tb-form-panel-title" translate>widget-config.appearance</div> <div class="tb-form-panel-title" translate>widget-config.appearance</div>
<div #appearanceProperties><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></div> <div #appearanceProperties><ng-content select=".tb-scada-symbol-appearance-properties"></ng-content></div>
<div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between" [class]="propertyRow.rowClass"> <div *ngFor="let propertyRow of propertyRows" class="tb-form-row space-between" [class]="propertyRow.rowClass">
<mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width" formControlName="{{ propertyRow.switch.id }}"> <mat-slide-toggle *ngIf="propertyRow.switch" class="mat-slide fixed-title-width margin" formControlName="{{ propertyRow.switch.id }}">
{{ propertyRow.label | customTranslate }} {{ propertyRow.label | customTranslate }}
</mat-slide-toggle> </mat-slide-toggle>
<div *ngIf="!propertyRow.switch" class="fixed-title-width">{{ propertyRow.label | customTranslate }}</div> <div *ngIf="!propertyRow.switch" class="fixed-title-width">{{ propertyRow.label | customTranslate }}</div>

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

@ -113,7 +113,7 @@ export class WidgetComponentService {
hasBasicMode: this.utils.editWidgetInfo.hasBasicMode, hasBasicMode: this.utils.editWidgetInfo.hasBasicMode,
basicModeDirective: this.utils.editWidgetInfo.basicModeDirective, basicModeDirective: this.utils.editWidgetInfo.basicModeDirective,
defaultConfig: this.utils.editWidgetInfo.defaultConfig defaultConfig: this.utils.editWidgetInfo.defaultConfig
}, new WidgetTypeId('1'), new TenantId( NULL_UUID ), undefined }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), undefined, undefined
); );
} }
const initSubject = new ReplaySubject<void>(); const initSubject = new ReplaySubject<void>();

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

@ -141,9 +141,8 @@ import {
import { import {
TypeValuePanelComponent TypeValuePanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { import {
ModbusRpcParametersComponent ModbusRpcParametersComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component';
import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
@ -233,7 +232,6 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
KeyValueIsNotEmptyPipe, KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent, ModbusBasicConfigComponent,
EllipsisChipListDirective, EllipsisChipListDirective,
TruncateWithTooltipDirective,
ModbusRpcParametersComponent, ModbusRpcParametersComponent,
], ],
exports: [ exports: [

4
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -169,12 +169,12 @@
</div> </div>
</div> </div>
<div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content"> <div [fxShow]="selectedOption === 'mobile'" [formGroup]="layoutSettings" class="mat-content">
<div class="tb-form-panel"> <div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="mobileHide"> <mat-slide-toggle class="mat-slide" formControlName="mobileHide">
{{ 'widget-config.mobile-hide' | translate }} {{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="desktopHide"> <mat-slide-toggle class="mat-slide" formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }} {{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle> </mat-slide-toggle>

16
ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts

@ -38,7 +38,7 @@ import {
JsonSchema, JsonSchema,
JsonSettingsSchema, JsonSettingsSchema,
TargetDevice, TargetDevice,
TargetDeviceType, targetDeviceValid, targetDeviceValid,
Widget, Widget,
WidgetConfigMode, WidgetConfigMode,
widgetType widgetType
@ -84,8 +84,8 @@ import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module'; import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module';
import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component';
import Timeout = NodeJS.Timeout;
import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models'; import { DataKeySettingsFunction } from '@home/components/widget/config/data-keys.component.models';
import Timeout = NodeJS.Timeout;
const emptySettingsSchema: JsonSchema = { const emptySettingsSchema: JsonSchema = {
type: 'object', type: 'object',
@ -152,7 +152,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
@Input() @Input()
@coerceBoolean() @coerceBoolean()
scada = false; showLayoutConfig = true;
@Input()
@coerceBoolean()
isDefaultBreakpoint = true;
@Input() disabled: boolean; @Input() disabled: boolean;
@ -345,10 +349,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
value: 'actions' value: 'actions'
} }
); );
if (!this.scada) { if (this.showLayoutConfig) {
this.headerOptions.push( this.headerOptions.push(
{ {
name: this.translate.instant('widget-config.mobile'), name: this.isDefaultBreakpoint
? this.translate.instant('widget-config.mobile')
: this.translate.instant('widget-config.list-layout'),
value: 'mobile' value: 'mobile'
} }
); );

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

@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import '../scss/constants';
.tb-widget-container { .tb-widget-container {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -182,12 +184,30 @@ gridster-item:hover {
line-height: 12px; line-height: 12px;
font-weight: 500; font-weight: 500;
color: rgba(0, 0, 0, 0.76); color: rgba(0, 0, 0, 0.76);
.tb-widget-actions-panel { .tb-widget-action-container {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
place-content: center flex-start; gap: 6px;
align-items: center;
gap: 8px; .tb-widget-actions-panel {
display: flex;
flex-direction: row;
place-content: center space-between;
align-items: center;
gap: 8px;
}
.tb-widget-reference-panel {
display: flex;
flex-direction: row;
place-content: center space-between;
align-items: center;
gap: 8px;
font-weight: normal;
color: $tb-primary-color;
font-size: 11px;
padding: 4px 4px 4px 6px;
border-radius: 2px;
}
} }
} }
} }

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

@ -40,6 +40,7 @@ import { GridsterItemComponent } from 'angular-gridster2';
import { UtilsService } from '@core/services/utils.service'; import { UtilsService } from '@core/services/utils.service';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { from } from 'rxjs'; import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
export enum WidgetComponentActionType { export enum WidgetComponentActionType {
MOUSE_DOWN, MOUSE_DOWN,
@ -47,7 +48,8 @@ export enum WidgetComponentActionType {
CONTEXT_MENU, CONTEXT_MENU,
EDIT, EDIT,
EXPORT, EXPORT,
REMOVE REMOVE,
REPLACE_REFERENCE_WITH_WIDGET_COPY,
} }
export class WidgetComponentAction { export class WidgetComponentAction {
@ -117,6 +119,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>(); widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
hovered = false; hovered = false;
isReferenceWidget = false;
get widgetEditActionsEnabled(): boolean { get widgetEditActionsEnabled(): boolean {
return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen; return (this.isEditActionEnabled || this.isRemoveActionEnabled || this.isExportActionEnabled) && !this.widget?.isFullscreen;
@ -130,6 +133,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private renderer: Renderer2, private renderer: Renderer2,
private container: ViewContainerRef, private container: ViewContainerRef,
private dashboardUtils: DashboardUtilsService,
private utils: UtilsService) { private utils: UtilsService) {
super(store); super(store);
} }
@ -144,7 +148,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
$(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent)); $(this.gridsterItem.el).on('mousedown', (e) => this.onMouseDown(e.originalEvent));
$(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent)); $(this.gridsterItem.el).on('click', (e) => this.onClicked(e.originalEvent));
$(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent)); $(this.gridsterItem.el).on('contextmenu', (e) => this.onContextMenu(e.originalEvent));
this.initEditWidgetActionTooltip(); const dashboardElement = this.widget.widgetContext.dashboardPageElement;
if (dashboardElement) {
this.initEditWidgetActionTooltip(dashboardElement);
}
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
@ -216,6 +223,13 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}); });
} }
onReplaceReferenceWithWidgetCopy(event: MouseEvent) {
this.widgetComponentAction.emit({
event,
actionType: WidgetComponentActionType.REPLACE_REFERENCE_WITH_WIDGET_COPY
});
}
onExport(event: MouseEvent) { onExport(event: MouseEvent) {
this.widgetComponentAction.emit({ this.widgetComponentAction.emit({
event, event,
@ -240,9 +254,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
} }
} }
private initEditWidgetActionTooltip() { private initEditWidgetActionTooltip(parent: HTMLElement) {
from(import('tooltipster')).subscribe(() => { from(import('tooltipster')).subscribe(() => {
$(this.gridsterItem.el).tooltipster({ $(this.gridsterItem.el).tooltipster({
parent: $(parent),
delay: this.widget.selected ? [0, 10000000] : [0, 100], delay: this.widget.selected ? [0, 10000000] : [0, 100],
distance: 2, distance: 2,
zIndex: 151, zIndex: 151,
@ -262,7 +277,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
content: '', content: '',
functionPosition: (instance, helper, position) => { functionPosition: (instance, helper, position) => {
const clientRect = helper.origin.getBoundingClientRect(); const clientRect = helper.origin.getBoundingClientRect();
position.coord.left = clientRect.right - position.size.width; const container = parent.getBoundingClientRect();
position.coord.left = clientRect.right - position.size.width - container.left;
position.coord.top = position.coord.top - container.top;
position.target = clientRect.right; position.target = clientRect.right;
return position; return position;
}, },
@ -280,6 +297,11 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
functionAfter: () => { functionAfter: () => {
this.hovered = false; this.hovered = false;
this.cd.markForCheck(); this.cd.markForCheck();
},
functionBefore: () => {
this.widget.isReference = this.dashboardUtils.isReferenceWidget(
this.widget.widgetContext.dashboard.stateController.dashboardCtrl.dashboardCtx.getDashboard(), this.widget.widgetId);
componentRef.instance.cd.detectChanges();
} }
}); });
this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance'); this.editWidgetActionsTooltip = $(this.gridsterItem.el).tooltipster('instance');
@ -327,29 +349,43 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
} }
@Component({ @Component({
template: `<div class="tb-widget-actions-panel"> template: `
<button mat-icon-button class="tb-mat-20" <div class="tb-widget-action-container">
[fxShow]="container.isEditActionEnabled" <div class="tb-widget-reference-panel tb-primary-fill" *ngIf="container.widget.isReference">
(click)="container.onEdit($event)" {{ 'widget.reference' | translate }}
matTooltip="{{ 'widget.edit' | translate }}" <button mat-icon-button class="tb-mat-16"
matTooltipPosition="above"> color="primary"
<tb-icon>edit</tb-icon> [fxShow]="container.isEditActionEnabled"
</button> (click)="container.onReplaceReferenceWithWidgetCopy($event)"
<button mat-icon-button class="tb-mat-20" matTooltip="{{ 'widget.replace-reference-with-widget-copy' | translate }}"
[fxShow]="container.isExportActionEnabled" matTooltipPosition="above">
(click)="container.onExport($event)" <tb-icon matButtonIcon>mdi:file-replace-outline</tb-icon>
matTooltip="{{ 'widget.export' | translate }}" </button>
matTooltipPosition="above"> </div>
<tb-icon>file_download</tb-icon> <div class="tb-widget-actions-panel">
</button> <button mat-icon-button class="tb-mat-20"
<button mat-icon-button class="tb-mat-20" [fxShow]="container.isEditActionEnabled"
[fxShow]="container.isRemoveActionEnabled" (click)="container.onEdit($event)"
(click)="container.onRemove($event);" matTooltip="{{ 'widget.edit' | translate }}"
matTooltip="{{ 'widget.remove' | translate }}" matTooltipPosition="above">
matTooltipPosition="above"> <tb-icon>edit</tb-icon>
<tb-icon>close</tb-icon> </button>
</button> <button mat-icon-button class="tb-mat-20"
</div>`, [fxShow]="container.isExportActionEnabled"
(click)="container.onExport($event)"
matTooltip="{{ 'widget.export' | translate }}"
matTooltipPosition="above">
<tb-icon>file_download</tb-icon>
</button>
<button mat-icon-button class="tb-mat-20"
[fxShow]="container.isRemoveActionEnabled"
(click)="container.onRemove($event);"
matTooltip="{{ 'widget.remove' | translate }}"
matTooltipPosition="above">
<tb-icon>close</tb-icon>
</button>
</div>
</div>`,
styles: [], styles: [],
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
@ -361,7 +397,8 @@ export class EditWidgetActionsTooltipComponent implements AfterViewInit {
@Output() @Output()
viewInited = new EventEmitter(); viewInited = new EventEmitter();
constructor(public element: ElementRef<HTMLElement>) { constructor(public element: ElementRef<HTMLElement>,
public cd: ChangeDetectorRef) {
} }
ngAfterViewInit() { ngAfterViewInit() {

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

@ -17,5 +17,5 @@
--> -->
<a mat-button routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'subset', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}"> <a mat-button routerLinkActive="tb-active" [routerLinkActiveOptions]="{paths: 'subset', queryParams: 'ignored', matrixParams: 'ignored', fragment: 'ignored'}" routerLink="{{section.path}}">
<tb-icon matButtonIcon *ngIf="section.icon !== null">{{section.icon}}</tb-icon> <tb-icon matButtonIcon *ngIf="section.icon !== null">{{section.icon}}</tb-icon>
<span>{{section.name | translate}}</span> <span>{{section.customTranslate ? (section.name | customTranslate) : (section.name | translate)}}</span>
</a> </a>

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

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

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

@ -65,13 +65,14 @@ export interface WidgetContextMenuItem extends ContextMenuItem {
export interface DashboardCallbacks { export interface DashboardCallbacks {
onEditWidget?: ($event: Event, widget: Widget) => void; onEditWidget?: ($event: Event, widget: Widget) => void;
replaceReferenceWithWidgetCopy?: ($event: Event, widget: Widget) => void;
onExportWidget?: ($event: Event, widget: Widget, widgeTitle: string) => void; onExportWidget?: ($event: Event, widget: Widget, widgeTitle: string) => void;
onRemoveWidget?: ($event: Event, widget: Widget) => void; onRemoveWidget?: ($event: Event, widget: Widget) => void;
onWidgetMouseDown?: ($event: Event, widget: Widget) => void; onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
onDashboardMouseDown?: ($event: Event) => void; onDashboardMouseDown?: ($event: Event) => void;
onWidgetClicked?: ($event: Event, widget: Widget) => void; onWidgetClicked?: ($event: Event, widget: Widget) => void;
prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>; prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>;
prepareWidgetContextMenu?: ($event: Event, widget: Widget) => Array<WidgetContextMenuItem>; prepareWidgetContextMenu?: ($event: Event, widget: Widget, isReference: boolean) => Array<WidgetContextMenuItem>;
} }
export interface IDashboardComponent { export interface IDashboardComponent {
@ -343,6 +344,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private selectedCallback: (selected: boolean) => void = () => {}; private selectedCallback: (selected: boolean) => void = () => {};
isFullscreen = false; isFullscreen = false;
isReference = false;
color: string; color: string;
backgroundColor: string; backgroundColor: string;

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

@ -180,6 +180,10 @@ export class WidgetContext {
} }
} }
get dashboardPageElement(): HTMLElement {
return this.dashboard?.stateController?.dashboardCtrl?.elRef?.nativeElement;
}
authService: AuthService; authService: AuthService;
deviceService: DeviceService; deviceService: DeviceService;
assetService: AssetService; assetService: AssetService;
@ -665,7 +669,7 @@ export const detailsToWidgetInfo = (widgetTypeDetailsEntity: WidgetTypeDetails):
}; };
export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
createdTime: number): WidgetType => { createdTime: number, version: number): WidgetType => {
const descriptor: WidgetTypeDescriptor = { const descriptor: WidgetTypeDescriptor = {
type: widgetInfo.type, type: widgetInfo.type,
sizeX: widgetInfo.sizeX, sizeX: widgetInfo.sizeX,
@ -688,6 +692,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
id, id,
tenantId, tenantId,
createdTime, createdTime,
version,
fqn: widgetTypeFqn(widgetInfo.fullFqn), fqn: widgetTypeFqn(widgetInfo.fullFqn),
name: widgetInfo.widgetName, name: widgetInfo.widgetName,
deprecated: widgetInfo.deprecated, deprecated: widgetInfo.deprecated,
@ -697,8 +702,8 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
}; };
export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId, export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
createdTime: number): WidgetTypeDetails => { createdTime: number, version: number): WidgetTypeDetails => {
const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime); const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime, version);
return { return {
...widgetTypeEntity, ...widgetTypeEntity,
description: widgetInfo.description, description: widgetInfo.description,

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

@ -45,6 +45,7 @@ import { ImageResourceType, IMAGES_URL_PREFIX, ResourceSubType } from '@shared/m
import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component'; import { ScadaSymbolComponent } from '@home/pages/scada-symbol/scada-symbol.component';
import { ImageService } from '@core/http/image.service'; import { ImageService } from '@core/http/image.service';
import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models'; import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { MenuId } from '@core/services/menu.models';
@Injectable() @Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -79,8 +80,7 @@ const routes: Routes = [
data: { data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: { breadcrumb: {
label: 'admin.resources', menuId: MenuId.resources
icon: 'folder'
} }
}, },
children: [ children: [
@ -97,8 +97,7 @@ const routes: Routes = [
path: 'images', path: 'images',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'image.gallery', menuId: MenuId.images
icon: 'filter'
} }
}, },
children: [ children: [
@ -117,8 +116,7 @@ const routes: Routes = [
path: 'scada-symbols', path: 'scada-symbols',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'scada.symbols', menuId: MenuId.scada_symbols
icon: 'view_in_ar'
} }
}, },
children: [ children: [
@ -153,8 +151,7 @@ const routes: Routes = [
path: 'resources-library', path: 'resources-library',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'resource.resources-library', menuId: MenuId.resources_library
icon: 'mdi:rhombus-split'
} }
}, },
children: [ children: [
@ -196,8 +193,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
showMainLoadingBar: false, showMainLoadingBar: false,
breadcrumb: { breadcrumb: {
label: 'admin.settings', menuId: MenuId.settings
icon: 'settings'
} }
}, },
children: [ children: [
@ -220,8 +216,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN], auth: [Authority.SYS_ADMIN],
title: 'admin.general-settings', title: 'admin.general-settings',
breadcrumb: { breadcrumb: {
label: 'admin.general', menuId: MenuId.general
icon: 'settings_applications'
} }
} }
}, },
@ -233,8 +228,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN], auth: [Authority.SYS_ADMIN],
title: 'admin.outgoing-mail-settings', title: 'admin.outgoing-mail-settings',
breadcrumb: { breadcrumb: {
label: 'admin.outgoing-mail', menuId: MenuId.mail_server
icon: 'mail'
} }
} }
}, },
@ -246,8 +240,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'admin.notifications-settings', title: 'admin.notifications-settings',
breadcrumb: { breadcrumb: {
label: 'admin.notifications', menuId: MenuId.notification_settings
icon: 'mdi:message-badge'
} }
} }
}, },
@ -255,8 +248,7 @@ const routes: Routes = [
path: 'queues', path: 'queues',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'admin.queues', menuId: MenuId.queues
icon: 'swap_calls'
} }
}, },
children: [ children: [
@ -297,8 +289,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'admin.home-settings', title: 'admin.home-settings',
breadcrumb: { breadcrumb: {
label: 'admin.home', menuId: MenuId.home_settings
icon: 'settings_applications'
} }
} }
}, },
@ -310,8 +301,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'admin.repository-settings', title: 'admin.repository-settings',
breadcrumb: { breadcrumb: {
label: 'admin.repository', menuId: MenuId.repository_settings
icon: 'manage_history'
} }
} }
}, },
@ -323,8 +313,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'admin.auto-commit-settings', title: 'admin.auto-commit-settings',
breadcrumb: { breadcrumb: {
label: 'admin.auto-commit', menuId: MenuId.auto_commit_settings
icon: 'settings_backup_restore'
} }
} }
}, },
@ -336,8 +325,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN], auth: [Authority.SYS_ADMIN],
title: 'admin.mobile-app.mobile-app', title: 'admin.mobile-app.mobile-app',
breadcrumb: { breadcrumb: {
label: 'admin.mobile-app.mobile-app', menuId: MenuId.mobile_app_settings
icon: 'smartphone'
} }
} }
}, },
@ -373,8 +361,7 @@ const routes: Routes = [
data: { data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN], auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: { breadcrumb: {
label: 'security.security', menuId: MenuId.security_settings
icon: 'security'
} }
}, },
children: [ children: [
@ -397,8 +384,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN], auth: [Authority.SYS_ADMIN],
title: 'admin.general', title: 'admin.general',
breadcrumb: { breadcrumb: {
label: 'admin.general', menuId: MenuId.security_settings_general
icon: 'settings_applications'
} }
} }
}, },
@ -410,8 +396,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN], auth: [Authority.SYS_ADMIN],
title: 'admin.2fa.2fa', title: 'admin.2fa.2fa',
breadcrumb: { breadcrumb: {
label: 'admin.2fa.2fa', menuId: MenuId.two_fa
icon: 'mdi:two-factor-authentication'
} }
} }
}, },
@ -423,8 +408,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN], auth: [Authority.SYS_ADMIN],
title: 'admin.oauth2.oauth2', title: 'admin.oauth2.oauth2',
breadcrumb: { breadcrumb: {
label: 'admin.oauth2.oauth2', menuId: MenuId.oauth2
icon: 'mdi:shield-account'
} }
}, },
resolve: { resolve: {

4
ui-ngx/src/app/modules/home/pages/alarm/alarm-routing.module.ts

@ -21,6 +21,7 @@ import { Observable } from 'rxjs';
import { OAuth2Service } from '@core/http/oauth2.service'; import { OAuth2Service } from '@core/http/oauth2.service';
import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component'; import { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmsMode } from '@shared/models/alarm.models'; import { AlarmsMode } from '@shared/models/alarm.models';
import { MenuId } from '@core/services/menu.models';
@Injectable() @Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> { export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -41,8 +42,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'alarm.alarms', title: 'alarm.alarms',
breadcrumb: { breadcrumb: {
label: 'alarm.alarms', menuId: MenuId.alarms
icon: 'mdi:alert-outline'
}, },
isPage: true, isPage: true,
alarmsMode: AlarmsMode.ALL alarmsMode: AlarmsMode.ALL

4
ui-ngx/src/app/modules/home/pages/api-usage/api-usage-routing.module.ts

@ -21,6 +21,7 @@ import { ApiUsageComponent } from '@home/pages/api-usage/api-usage.component';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { MenuId } from '@core/services/menu.models';
const apiUsageDashboardJson = '/assets/dashboard/api_usage.json'; const apiUsageDashboardJson = '/assets/dashboard/api_usage.json';
@ -38,8 +39,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'api-usage.api-usage', title: 'api-usage.api-usage',
breadcrumb: { breadcrumb: {
label: 'api-usage.api-usage', menuId: MenuId.api_usage
icon: 'insert_chart'
} }
}, },
resolve: { resolve: {

4
ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts

@ -24,14 +24,14 @@ import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { AssetProfilesTableConfigResolver } from './asset-profiles-table-config.resolver'; import { AssetProfilesTableConfigResolver } from './asset-profiles-table-config.resolver';
import { MenuId } from '@core/services/menu.models';
export const assetProfilesRoutes: Routes = [ export const assetProfilesRoutes: Routes = [
{ {
path: 'assetProfiles', path: 'assetProfiles',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'asset-profile.asset-profiles', menuId: MenuId.asset_profiles
icon: 'mdi:alpha-a-box'
} }
}, },
children: [ children: [

4
ui-ngx/src/app/modules/home/pages/asset/asset-routing.module.ts

@ -24,14 +24,14 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { MenuId } from '@core/services/menu.models';
export const assetRoutes: Routes = [ export const assetRoutes: Routes = [
{ {
path: 'assets', path: 'assets',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'asset.assets', menuId: MenuId.assets
icon: 'domain'
} }
}, },
children: [ children: [

4
ui-ngx/src/app/modules/home/pages/audit-log/audit-log-routing.module.ts

@ -18,6 +18,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { Authority } from '@shared/models/authority.enum'; import { Authority } from '@shared/models/authority.enum';
import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component'; import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component';
import { MenuId } from '@core/services/menu.models';
export const auditLogsRoutes: Routes = [ export const auditLogsRoutes: Routes = [
{ {
@ -27,8 +28,7 @@ export const auditLogsRoutes: Routes = [
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
title: 'audit-log.audit-logs', title: 'audit-log.audit-logs',
breadcrumb: { breadcrumb: {
label: 'audit-log.audit-logs', menuId: MenuId.audit_log
icon: 'track_changes'
}, },
isPage: true isPage: true
} }

4
ui-ngx/src/app/modules/home/pages/customer/customer-routing.module.ts

@ -31,14 +31,14 @@ import { EdgesTableConfigResolver } from '@home/pages/edge/edges-table-config.re
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { MenuId } from '@core/services/menu.models';
const routes: Routes = [ const routes: Routes = [
{ {
path: 'customers', path: 'customers',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'customer.customers', menuId: MenuId.customers
icon: 'supervisor_account'
} }
}, },
children: [ children: [

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

@ -33,6 +33,7 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { MenuId } from '@core/services/menu.models';
@Injectable() @Injectable()
export class DashboardResolver implements Resolve<Dashboard> { export class DashboardResolver implements Resolve<Dashboard> {
@ -66,8 +67,7 @@ const routes: Routes = [
path: 'dashboards', path: 'dashboards',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'dashboard.dashboards', menuId: MenuId.dashboards
icon: 'dashboard'
} }
}, },
children: [ children: [

4
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-routing.module.ts

@ -24,14 +24,14 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { MenuId } from '@core/services/menu.models';
export const deviceProfilesRoutes: Routes = [ export const deviceProfilesRoutes: Routes = [
{ {
path: 'deviceProfiles', path: 'deviceProfiles',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'device-profile.device-profiles', menuId: MenuId.device_profiles
icon: 'mdi:alpha-d-box'
} }
}, },
children: [ children: [

4
ui-ngx/src/app/modules/home/pages/device/device-routing.module.ts

@ -26,14 +26,14 @@ import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { assetRoutes } from '@home/pages/asset/asset-routing.module'; import { assetRoutes } from '@home/pages/asset/asset-routing.module';
import { entityViewRoutes } from '@home/pages/entity-view/entity-view-routing.module'; import { entityViewRoutes } from '@home/pages/entity-view/entity-view-routing.module';
import { MenuId } from '@core/services/menu.models';
export const deviceRoutes: Routes = [ export const deviceRoutes: Routes = [
{ {
path: 'devices', path: 'devices',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'device.devices', menuId: MenuId.devices
icon: 'devices_other'
} }
}, },
children: [ children: [

10
ui-ngx/src/app/modules/home/pages/edge/edge-routing.module.ts

@ -41,14 +41,14 @@ import {
} from '@home/pages/rulechain/rulechain-routing.module'; } from '@home/pages/rulechain/rulechain-routing.module';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { MenuId } from '@core/services/menu.models';
const routes: Routes = [ const routes: Routes = [
{ {
path: 'edgeManagement', path: 'edgeManagement',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'edge.management', menuId: MenuId.edge_management
icon: 'settings_input_antenna'
} }
}, },
children: [ children: [
@ -64,8 +64,7 @@ const routes: Routes = [
path: 'instances', path: 'instances',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'edge.instances', menuId: MenuId.edges
icon: 'router'
} }
}, },
children: [ children: [
@ -307,8 +306,7 @@ const routes: Routes = [
path: 'ruleChains', path: 'ruleChains',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'edge.rulechain-templates', menuId: MenuId.rulechain_templates
icon: 'settings_ethernet'
} }
}, },
children: [ children: [

4
ui-ngx/src/app/modules/home/pages/entity-view/entity-view-routing.module.ts

@ -24,14 +24,14 @@ import { EntityDetailsPageComponent } from '@home/components/entity/entity-detai
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard'; import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb'; import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { MenuId } from '@core/services/menu.models';
export const entityViewRoutes: Routes = [ export const entityViewRoutes: Routes = [
{ {
path: 'entityViews', path: 'entityViews',
data: { data: {
breadcrumb: { breadcrumb: {
label: 'entity-view.entity-views', menuId: MenuId.entity_views
icon: 'view_quilt'
} }
}, },
children: [ children: [

4
ui-ngx/src/app/modules/home/pages/features/features-routing.module.ts

@ -19,6 +19,7 @@ import { Authority } from '@shared/models/authority.enum';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { otaUpdatesRoutes } from '@home/pages/ota-update/ota-update-routing.module'; import { otaUpdatesRoutes } from '@home/pages/ota-update/ota-update-routing.module';
import { vcRoutes } from '@home/pages/vc/vc-routing.module'; import { vcRoutes } from '@home/pages/vc/vc-routing.module';
import { MenuId } from '@core/services/menu.models';
const routes: Routes = [ const routes: Routes = [
{ {
@ -26,8 +27,7 @@ const routes: Routes = [
data: { data: {
auth: [Authority.TENANT_ADMIN], auth: [Authority.TENANT_ADMIN],
breadcrumb: { breadcrumb: {
label: 'feature.advanced-features', menuId: MenuId.features
icon: 'construction'
} }
}, },
children: [ children: [

4
ui-ngx/src/app/modules/home/pages/home-links/home-links-routing.module.ts

@ -34,6 +34,7 @@ import {
import { EntityKeyType } from '@shared/models/query/query.models'; import { EntityKeyType } from '@shared/models/query/query.models';
import { ResourcesService } from '@core/services/resources.service'; import { ResourcesService } from '@core/services/resources.service';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull } from '@core/utils';
import { MenuId } from '@core/services/menu.models';
const sysAdminHomePageJson = '/assets/dashboard/sys_admin_home_page.json'; const sysAdminHomePageJson = '/assets/dashboard/sys_admin_home_page.json';
const tenantAdminHomePageJson = '/assets/dashboard/tenant_admin_home_page.json'; const tenantAdminHomePageJson = '/assets/dashboard/tenant_admin_home_page.json';
@ -125,8 +126,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'home.home', title: 'home.home',
breadcrumb: { breadcrumb: {
label: 'home.home', menuId: MenuId.home
icon: 'home'
} }
}, },
resolve: { resolve: {

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

@ -28,7 +28,7 @@
<mat-grid-tile *ngFor="let place of section.places"> <mat-grid-tile *ngFor="let place of section.places">
<a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}"> <a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">
<tb-icon matButtonIcon class="tb-mat-96">{{place.icon}}</tb-icon> <tb-icon matButtonIcon class="tb-mat-96">{{place.icon}}</tb-icon>
<span translate>{{place.name}}</span> <span>{{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}}</span>
</a> </a>
</mat-grid-tile> </mat-grid-tile>
</mat-grid-list> </mat-grid-list>

19
ui-ngx/src/app/modules/home/pages/notification/notification-routing.module.ts

@ -25,6 +25,7 @@ import { RecipientTableConfigResolver } from '@home/pages/notification/recipient
import { TemplateTableConfigResolver } from '@home/pages/notification/template/template-table-config.resolver'; import { TemplateTableConfigResolver } from '@home/pages/notification/template/template-table-config.resolver';
import { RuleTableConfigResolver } from '@home/pages/notification/rule/rule-table-config.resolver'; import { RuleTableConfigResolver } from '@home/pages/notification/rule/rule-table-config.resolver';
import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component'; import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component';
import { MenuId } from '@core/services/menu.models';
const routes: Routes = [ const routes: Routes = [
{ {
@ -33,8 +34,7 @@ const routes: Routes = [
data: { data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN], auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN],
breadcrumb: { breadcrumb: {
label: 'notification.notification-center', menuId: MenuId.notifications_center
icon: 'mdi:message-badge'
}, },
routerTabsHeaderComponent: SendNotificationButtonComponent routerTabsHeaderComponent: SendNotificationButtonComponent
}, },
@ -54,8 +54,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN], auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN],
title: 'notification.inbox', title: 'notification.inbox',
breadcrumb: { breadcrumb: {
label: 'notification.inbox', menuId: MenuId.notification_inbox
icon: 'inbox'
} }
}, },
resolve: { resolve: {
@ -69,8 +68,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'notification.sent', title: 'notification.sent',
breadcrumb: { breadcrumb: {
label: 'notification.sent', menuId: MenuId.notification_sent
icon: 'outbox'
} }
}, },
resolve: { resolve: {
@ -84,8 +82,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'notification.templates', title: 'notification.templates',
breadcrumb: { breadcrumb: {
label: 'notification.templates', menuId: MenuId.notification_templates
icon: 'mdi:message-draw'
} }
}, },
resolve: { resolve: {
@ -99,8 +96,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'notification.recipients', title: 'notification.recipients',
breadcrumb: { breadcrumb: {
label: 'notification.recipients', menuId: MenuId.notification_recipients
icon: 'contacts'
}, },
}, },
resolve: { resolve: {
@ -114,8 +110,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN], auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'notification.rules', title: 'notification.rules',
breadcrumb: { breadcrumb: {
label: 'notification.rules', menuId: MenuId.notification_rules
icon: 'mdi:message-cog'
} }
}, },
resolve: { resolve: {

1
ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html

@ -104,6 +104,7 @@
</ng-template> </ng-template>
<ng-template #recipientsList> <ng-template #recipientsList>
<tb-entity-list <tb-entity-list
syncIdsWithDB
required required
formControlName="targets" formControlName="targets"
labelText="{{ 'notification.recipients' | translate }}" labelText="{{ 'notification.recipients' | translate }}"

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

Loading…
Cancel
Save