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",
"name": "{i18n:scada.symbol.value-box}",
"type": "color",
"default": "#FFFFFF",
"default": "#F3F3F3",
"required": null,
"subLabel": null,
"divider": null,
@ -633,7 +633,7 @@
</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"/>
</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"/>
<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>

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",
"horizontal_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;
}
@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. " +
"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)
@ -160,7 +160,7 @@ public class AdminController extends BaseController {
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)
@PreAuthorize("hasAuthority('SYS_ADMIN')")
@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. "
+ SYSTEM_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('SYS_ADMIN')")

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

@ -20,8 +20,8 @@ server:
address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
# Server bind port
port: "${HTTP_BIND_PORT:8080}"
# Server forward headers strategy
forward_headers_strategy: "${HTTP_FORWARD_HEADERS_STRATEGY:NONE}"
# Server forward headers strategy. Required for SWAGGER UI when reverse proxy is used
forward_headers_strategy: "${HTTP_FORWARD_HEADERS_STRATEGY:framework}"
# Server SSL configuration
ssl:
# 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
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<>();
@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<>();
@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<>();
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
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;
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "JSON object with attributes to expose")
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();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
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.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes);
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.SESSION_TIMEOUT_MS_CONFIG, sessionTimeoutMs);
props.putAll(PropertyUtils.getProps(otherInline));
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();
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
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/
# Required for SWAGGER UI when reverse proxy is used
ENV HTTP_FORWARD_HEADERS_STRATEGY=framework
RUN chmod a+x /tmp/*.sh \
&& mv /tmp/start-tb-node.sh /usr/bin && \
(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,
id: WidgetTypeId,
createdTime: number,
version: number,
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,
defaultHttpOptionsFromConfig(config)).pipe(
tap((savedWidgetType) => {

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

@ -20,6 +20,7 @@ import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpParams,
HttpRequest,
HttpStatusCode
} from '@angular/common/http';
@ -32,6 +33,8 @@ import {
import { HasId } from '@shared/models/base-data';
import { HasVersion } from '@shared/models/entity.models';
import { getInterceptorConfig } from './interceptor.util';
import { isDefined } from '@core/utils';
import { InterceptorConfig } from '@core/interceptors/interceptor-config';
@Injectable()
export class EntityConflictInterceptor implements HttpInterceptor {
@ -67,8 +70,12 @@ export class EntityConflictInterceptor implements HttpInterceptor {
return this.openConflictDialog(request.body, error.error.message).pipe(
switchMap(result => {
if (result) {
return next.handle(this.updateRequestVersion(request));
if (isDefined(result)) {
if (result) {
return next.handle(this.updateRequestVersion(request));
}
(request.params as HttpParams & { interceptorConfig: InterceptorConfig }).interceptorConfig.ignoreErrors = true;
return throwError(() => error);
}
return of(null);
})
@ -82,7 +89,9 @@ export class EntityConflictInterceptor implements HttpInterceptor {
private openConflictDialog(entity: unknown & HasId & HasVersion, message: string): Observable<boolean> {
const dialogRef = this.dialog.open(EntityConflictDialogComponent, {
data: { message, entity }
disableClose: true,
data: { message, entity },
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
});
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 { TimeService } from '@core/services/time.service';
import {
BreakpointId,
breakpointIdIconMap,
breakpointIdTranslationMap,
BreakpointInfo,
BreakpointLayoutInfo,
BreakpointSystemId,
Dashboard,
DashboardConfiguration,
DashboardLayout,
DashboardLayoutId,
DashboardLayoutInfo,
DashboardLayoutsInfo,
DashboardState,
DashboardStateLayouts,
@ -41,6 +46,7 @@ import {
Widget,
WidgetConfig,
WidgetConfigMode,
WidgetSize,
widgetType,
WidgetTypeDescriptor
} 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 { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { BackgroundType, colorBackground, isBackgroundSettings } from '@shared/models/widget-settings.models';
import { MediaBreakpoints } from '@shared/models/constants';
import { TranslateService } from '@ngx-translate/core';
@Injectable({
providedIn: 'root'
})
export class DashboardUtilsService {
private systemBreakpoints: {[key in BreakpointSystemId]?: BreakpointInfo};
constructor(private utils: UtilsService,
private timeService: TimeService) {
private timeService: TimeService,
private translate: TranslateService) {
}
public validateAndUpdateDashboard(dashboard: Dashboard): Dashboard {
@ -523,6 +534,28 @@ export class DashboardUtilsService {
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 {
for (const stateId of Object.keys(states)) {
const state = states[stateId];
@ -542,16 +575,14 @@ export class DashboardUtilsService {
for (const l of Object.keys(state.layouts)) {
const layout: DashboardLayout = state.layouts[l];
if (layout) {
result[l] = {
widgetIds: [],
widgetLayouts: {},
gridSettings: {}
} as DashboardLayoutInfo;
for (const id of Object.keys(layout.widgets)) {
result[l].widgetIds.push(id);
result[l]= {
default: this.getBreakpointLayoutData(layout)
};
if (layout.breakpoints) {
for (const breakpoint of Object.keys(layout.breakpoints)) {
result[l][breakpoint] = this.getBreakpointLayoutData(layout.breakpoints[breakpoint]);
}
}
result[l].widgetLayouts = layout.widgets;
result[l].gridSettings = layout.gridSettings;
}
}
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> {
const widgetsArray: Array<Widget> = [];
const dashboardConfiguration = dashboard.configuration;
@ -586,11 +631,15 @@ export class DashboardUtilsService {
originalColumns?: number,
originalSize?: {sizeX: number; sizeY: number},
row?: number,
column?: number): void {
column?: number,
breakpoint = 'default'): void {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
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;
if (!widget.id) {
widget.id = this.utils.guid();
@ -648,11 +697,9 @@ export class DashboardUtilsService {
public removeWidgetFromLayout(dashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,
widgetId: string) {
const dashboardConfiguration = dashboard.configuration;
const states = dashboardConfiguration.states;
const state = states[targetState];
const layout = state.layouts[targetLayout];
widgetId: string,
breakpoint: BreakpointId) {
const layout = this.getDashboardLayoutConfig(dashboard.configuration.states[targetState].layouts[targetLayout], breakpoint);
delete layout.widgets[widgetId];
this.removeUnusedWidgets(dashboard);
}
@ -721,11 +768,19 @@ export class DashboardUtilsService {
for (const s of Object.keys(states)) {
const state = states[s];
for (const l of Object.keys(state.layouts)) {
const layout = state.layouts[l];
const layout: DashboardLayout = state.layouts[l];
if (layout.widgets[widgetId]) {
found = true;
break;
}
if (layout.breakpoints) {
for (const breakpoint of Object.keys(layout.breakpoints)) {
if (layout.breakpoints[breakpoint].widgets[widgetId]) {
found = true;
break;
}
}
}
}
}
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 { 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 {
Datasource,
@ -56,6 +56,7 @@ export interface WidgetReference {
widgetId: string;
originalSize: WidgetSize;
originalColumns: number;
breakpoint: string;
}
export interface RuleNodeConnection {
@ -85,7 +86,8 @@ export class ItemBufferService {
private ruleChainService: RuleChainService,
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 = {
datasourceAliases: {},
targetDeviceAlias: null
@ -93,8 +95,8 @@ export class ItemBufferService {
const filtersInfo: FiltersInfo = {
datasourceFilters: {}
};
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget);
const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
const datasources: Datasource[] = widget.type === widgetType.alarm ? [widget.config.alarmSource] : widget.config.datasources;
if (widget.config && dashboard.configuration
&& dashboard.configuration.entityAliases) {
@ -146,13 +148,14 @@ export class ItemBufferService {
};
}
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget);
public copyWidget(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: BreakpointId) {
const widgetItem = this.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_ITEM, widgetItem);
}
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId, widget: Widget): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget);
public copyWidgetReference(dashboard: Dashboard, sourceState: string, sourceLayout: DashboardLayoutId,
widget: Widget, breakpoint: BreakpointId): void {
const widgetReference = this.prepareWidgetReference(dashboard, sourceState, sourceLayout, widget, breakpoint);
this.storeSet(WIDGET_REFERENCE, widgetReference);
}
@ -160,11 +163,11 @@ export class ItemBufferService {
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);
if (widgetReference) {
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]) {
return true;
}
@ -174,7 +177,9 @@ export class ItemBufferService {
}
public pasteWidget(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition,
targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition,
onAliasesUpdateFunction: () => void,
onFiltersUpdateFunction: () => void): Observable<Widget> {
const widgetItem: WidgetItem = this.storeGet(WIDGET_ITEM);
@ -194,7 +199,7 @@ export class ItemBufferService {
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, aliasesInfo, filtersInfo,
onAliasesUpdateFunction, onFiltersUpdateFunction,
originalColumns, originalSize, targetRow, targetColumn).pipe(
originalColumns, originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget)
);
} else {
@ -202,8 +207,11 @@ export class ItemBufferService {
}
}
public pasteWidgetReference(targetDashboard: Dashboard, targetState: string,
targetLayout: DashboardLayoutId, position: WidgetPosition): Observable<Widget> {
public pasteWidgetReference(targetDashboard: Dashboard,
targetState: string,
targetLayout: DashboardLayoutId,
breakpoint: string,
position: WidgetPosition): Observable<Widget> {
const widgetReference: WidgetReference = this.storeGet(WIDGET_REFERENCE);
if (widgetReference) {
const widget = targetDashboard.configuration.widgets[widgetReference.widgetId];
@ -219,7 +227,7 @@ export class ItemBufferService {
return this.addWidgetToDashboard(targetDashboard, targetState,
targetLayout, widget, null,
null, null, null, originalColumns,
originalSize, targetRow, targetColumn).pipe(
originalSize, targetRow, targetColumn, breakpoint).pipe(
map(() => widget)
);
} else {
@ -239,7 +247,8 @@ export class ItemBufferService {
originalColumns: number,
originalSize: WidgetSize,
row: number,
column: number): Observable<Dashboard> {
column: number,
breakpoint = 'default'): Observable<Dashboard> {
let theDashboard: Dashboard;
if (dashboard) {
theDashboard = dashboard;
@ -270,7 +279,7 @@ export class ItemBufferService {
}
}
this.dashboardUtils.addWidgetToLayout(theDashboard, targetState, targetLayout, widget,
originalColumns, originalSize, row, column);
originalColumns, originalSize, row, column, breakpoint);
if (callAliasUpdateFunction) {
onAliasesUpdateFunction();
}
@ -387,35 +396,6 @@ export class ItemBufferService {
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 {
return {
alias: entityAlias.alias,
@ -432,16 +412,17 @@ export class ItemBufferService {
}
private prepareWidgetReference(dashboard: Dashboard, sourceState: string,
sourceLayout: DashboardLayoutId, widget: Widget): WidgetReference {
const originalColumns = this.getOriginalColumns(dashboard, sourceState, sourceLayout);
const originalSize = this.getOriginalSize(dashboard, sourceState, sourceLayout, widget);
sourceLayout: DashboardLayoutId, widget: Widget, breakpoint: BreakpointId): WidgetReference {
const originalColumns = this.dashboardUtils.getOriginalColumns(dashboard, sourceState, sourceLayout, breakpoint);
const originalSize = this.dashboardUtils.getOriginalSize(dashboard, sourceState, sourceLayout, widget, breakpoint);
return {
dashboardId: dashboard.id.id,
sourceState,
sourceLayout,
widgetId: widget.id,
originalSize,
originalColumns
originalColumns,
breakpoint
};
}

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

@ -14,11 +14,14 @@
/// 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 interface MenuSection extends HasUUID{
export interface MenuSection {
id: MenuId | string;
name: string;
fullName?: string;
type: MenuSectionType;
@ -26,17 +29,901 @@ export interface MenuSection extends HasUUID{
icon: string;
pages?: Array<MenuSection>;
opened?: boolean;
disabled?: boolean;
rootOnly?: boolean;
customTranslate?: boolean;
}
export interface HomeSection {
export interface MenuReference {
id: MenuId;
pages?: Array<MenuReference>;
}
export interface HomeSectionReference {
name: string;
places: Array<HomeSectionPlace>;
places: Array<MenuId>;
}
export interface HomeSectionPlace {
export interface HomeSection {
name: string;
icon: string;
path: string;
places: Array<MenuSection>;
}
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 { getCurrentOpenedMenuSections, selectAuth, selectIsAuthenticated } from '../auth/auth.selectors';
import { filter, map, take } from 'rxjs/operators';
import { HomeSection, MenuSection } from '@core/services/menu.models';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { Authority } from '@shared/models/authority.enum';
import { buildUserHome, buildUserMenu, HomeSection, MenuId, MenuSection } from '@core/services/menu.models';
import { Observable, ReplaySubject, Subject } from 'rxjs';
import { AuthState } from '@core/auth/auth.models';
import { NavigationEnd, Router } from '@angular/router';
@ -30,10 +29,11 @@ import { NavigationEnd, Router } from '@angular/router';
})
export class MenuService {
currentMenuSections: Array<MenuSection>;
menuSections$: Subject<Array<MenuSection>> = new BehaviorSubject<Array<MenuSection>>([]);
homeSections$: Subject<Array<HomeSection>> = new BehaviorSubject<Array<HomeSection>>([]);
availableMenuLinks$ = this.menuSections$.pipe(
private currentMenuSections: Array<MenuSection>;
private menuSections$: Subject<Array<MenuSection>> = new ReplaySubject<Array<MenuSection>>(1);
private homeSections$: Subject<Array<HomeSection>> = new ReplaySubject<Array<HomeSection>>(1);
private availableMenuSections$: Subject<Array<MenuSection>> = new ReplaySubject<Array<MenuSection>>(1);
private availableMenuLinks$ = this.menuSections$.pipe(
map((items) => this.allMenuLinks(items))
);
@ -57,23 +57,12 @@ export class MenuService {
this.store.pipe(select(selectAuth), take(1)).subscribe(
(authState: AuthState) => {
if (authState.authUser) {
let homeSections: Array<HomeSection>;
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.currentMenuSections = buildUserMenu(authState);
this.updateOpenedMenuSections();
this.menuSections$.next(this.currentMenuSections);
const availableMenuSections = this.allMenuSections(this.currentMenuSections);
this.availableMenuSections$.next(availableMenuSections);
const homeSections = buildUserHome(authState, availableMenuSections);
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> {
const result: Array<MenuSection> = [];
for (const section of sections) {
@ -1012,6 +91,17 @@ export class MenuService {
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>> {
return this.menuSections$;
}
@ -1024,7 +114,11 @@ export class MenuService {
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(
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"
[widget]="widget"
[hideHeader]="hideHeader"
[scada]="data.scada"
[showLayoutConfig]="showLayoutConfig"
[isDefaultBreakpoint]="isDefaultBreakpoint"
isAdd
formControlName="widgetConfig">
</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;
widget: Widget;
widgetInfo: WidgetInfo;
scada: boolean;
showLayoutConfig: boolean;
isDefaultBreakpoint: boolean;
}
@Component({
@ -60,6 +61,9 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
stateController: IStateController;
widget: Widget;
showLayoutConfig = true;
isDefaultBreakpoint = true;
widgetConfig: WidgetConfigComponentData;
previewMode = false;
@ -92,6 +96,8 @@ export class AddWidgetDialogComponent extends DialogComponent<AddWidgetDialogCom
this.aliasController = this.data.aliasController;
this.stateController = this.data.stateController;
this.widget = this.data.widget;
this.showLayoutConfig = this.data.showLayoutConfig;
this.isDefaultBreakpoint = this.data.isDefaultBreakpoint;
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>
<ng-container *ngIf="isEdit">
<div style="flex: 1 1 100%" fxHide.gt-sm></div>
<div fxFlex.lt-md fxLayout="row"
fxLayoutAlign.gt-sm="start center"
fxLayoutAlign="end center" fxLayoutGap.lt-lg="6px" fxLayoutGap.gt-md="12px">
<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>
fxLayoutAlign="end center" fxLayoutGap.lt-lg="3px" fxLayoutGap.lg="6px" fxLayoutGap.gt-lg="12px">
<mat-divider class="tb-toolbar-divider" vertical></mat-divider>
<button fxHide.gt-md mat-icon-button
matTooltip="{{'layout.manage' | translate}}"
matTooltipPosition="below"
@ -58,6 +47,9 @@
<mat-icon>view_compact</mat-icon>
{{'layout.layouts' | translate}}
</button>
<tb-select-dashboard-breakpoint
[dashboardCtrl]="this">
</tb-select-dashboard-breakpoint>
</div>
</ng-container>
<tb-states-component fxFlex.lt-md
@ -70,6 +62,21 @@
[syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states">
</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"
aria-label="dashboard_logo" class="dashboard_logo"/>
</div>
@ -88,7 +95,7 @@
(click)="toggleLayouts()">
<mat-icon>{{isRightLayoutOpened ? 'arrow_back' : 'menu'}}</mat-icon>
</button>
<button [fxShow]="!hideFullscreenButton()" mat-icon-button
<button [fxShow]="!hideFullscreenButton()" mat-icon-button fxHide.xs
matTooltip="{{(isFullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="below"
(click)="isFullscreen = !isFullscreen">
@ -230,13 +237,13 @@
</ng-container>
</ng-template>
<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 }}"
matTooltipPosition="below"
(click)="addWidget($event)">
<mat-icon>add</mat-icon>
</button>
<button fxHide.lt-sm mat-stroked-button
<button fxHide.lt-lg mat-stroked-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below"
(click)="addWidget($event)">
@ -282,7 +289,7 @@
</mat-form-field>
</section>
<mat-drawer-container class="tb-dashboard-layouts" fxFlex
[ngClass]="{ 'tb-shrinked' : isEditingWidget }">
[class]="{'tb-shrinked' : isEditingWidget}">
<mat-drawer *ngIf="layouts.right.show"
id="tb-right-layout"
[ngStyle]="{minWidth: rightLayoutSize.width,
@ -308,9 +315,11 @@
</mat-drawer>
<mat-drawer-content [fxShow]="layouts.main.show"
id="tb-main-layout"
[ngStyle]="{width: mainLayoutSize.width,
height: mainLayoutSize.height}">
[style]="{width: mainLayoutSize.width,
height: mainLayoutSize.height}">
<tb-dashboard-layout
[style.max-width]="mainLayoutSize.maxWidth"
[style.min-width]="mainLayoutSize.minWidth"
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
@ -321,6 +330,11 @@
[parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout>
<div class="layout-filler"
*ngIf="isEdit"
[fxShow]="showMainLayoutFiller">
{{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }}
</div>
</mat-drawer-content>
</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)">
@ -380,7 +394,8 @@
[widgetEditMode]="widgetEditMode"
[widget]="editingWidget"
[widgetLayout]="editingWidgetLayout"
[scada]="editingLayoutCtx?.gridSettings?.layoutType === LayoutType.scada"
[showLayoutConfig]="showLayoutConfigInEdit(editingLayoutCtx)"
[isDefaultBreakpoint]="editingLayoutCtx?.breakpoint === 'default'"
(revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()">
</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));
}
}
#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 {

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 { ActivatedRoute, Router } from '@angular/router';
import { UtilsService } from '@core/services/utils.service';
import { AuthService } from '@core/auth/auth.service';
import {
BreakpointId,
BreakpointInfo,
Dashboard,
DashboardConfiguration,
DashboardLayoutId,
@ -52,6 +53,7 @@ import {
GridSettings,
LayoutDimension,
LayoutType,
ViewFormatType,
WidgetLayout
} from '@app/shared/models/dashboard.models';
import { WINDOW } from '@core/services/window.service';
@ -67,7 +69,7 @@ import {
IDashboardController,
LayoutWidgetsArray
} from './dashboard-page.models';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
import { AuthUser } from '@shared/models/user.model';
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 { EntityService } from '@core/http/entity.service';
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 { DashboardService } from '@core/http/dashboard.service';
import {
@ -93,7 +95,6 @@ import {
WidgetContextMenuItem
} from '../../models/dashboard-component.models';
import { WidgetComponentService } from '../../components/widget/widget-component.service';
import { UntypedFormBuilder } from '@angular/forms';
import { ItemBufferService } from '@core/services/item-buffer.service';
import { MatDialog } from '@angular/material/dialog';
import {
@ -139,14 +140,14 @@ import {
DashboardImageDialogData,
DashboardImageDialogResult
} 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 { DOCUMENT } from '@angular/common';
import { IAliasController } from '@core/api/widget-api.models';
import { MatButton } from '@angular/material/button';
import { VersionControlComponent } from '@home/components/vc/version-control.component';
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 { TbPopoverComponent } from '@shared/components/popover.component';
import { ResizeObserver } from '@juggle/resize-observer';
@ -155,6 +156,7 @@ import {
MoveWidgetsDialogComponent,
MoveWidgetsDialogResult
} from '@home/components/dashboard-page/layout/move-widgets-dialog.component';
import { HttpStatusCode } from '@angular/common/http';
// @dynamic
@Component({
@ -262,7 +264,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
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%'};
private dashboardLogoCache: SafeUrl;
@ -275,6 +278,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
getDashboard: () => this.dashboard,
dashboardTimewindow: null,
state: null,
breakpoint: null,
stateController: null,
stateChanged: null,
stateId: null,
@ -287,26 +291,32 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
show: false,
layoutCtx: {
id: 'main',
breakpoint: 'default',
widgets: null,
widgetLayouts: {},
gridSettings: {},
ignoreLoading: true,
ctrl: null,
dashboardCtrl: this,
displayGrid: 'onDrag&Resize'
displayGrid: 'onDrag&Resize',
layoutData: null,
layoutDataChanged: new BehaviorSubject(null),
}
},
right: {
show: false,
layoutCtx: {
id: 'right',
breakpoint: 'default',
widgets: null,
widgetLayouts: {},
gridSettings: {},
ignoreLoading: true,
ctrl: null,
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;
private changeMobileSize = new Subject<boolean>();
constructor(protected store: Store<AppState>,
@Inject(WINDOW) private window: Window,
@Inject(DOCUMENT) private document: Document,
@ -348,7 +360,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private router: Router,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private authService: AuthService,
private entityService: EntityService,
private dialogService: DialogService,
private widgetComponentService: WidgetComponentService,
@ -356,7 +367,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private itembuffer: ItemBufferService,
private importExport: ImportExportService,
private mobileService: MobileService,
private fb: UntypedFormBuilder,
private dialog: MatDialog,
public translate: TranslateService,
private popoverService: TbPopoverService,
@ -366,12 +376,11 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer,
public elRef: ElementRef,
private injector: Injector) {
super(store);
if (isDefinedAndNotNull(embeddedValue)) {
this.embedded = embeddedValue;
if (isDefinedAndNotNull(this.embeddedValue)) {
this.embedded = this.embeddedValue;
}
}
@ -411,13 +420,42 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
));
}
this.rxSubscriptions.push(this.breakpointObserver
.observe(MediaBreakpoints['gt-sm'])
.subscribe((state: BreakpointState) => {
this.isMobile = !state.matches;
this.rxSubscriptions.push(
this.changeMobileSize.pipe(
distinctUntilChanged(),
).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();
}
));
)
);
if (this.isMobileApp && this.syncStateWithQueryParam) {
this.mobileService.registerToggleLayoutFunction(() => {
setTimeout(() => {
@ -565,6 +603,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
if (this.dashboardResize$) {
this.dashboardResize$.disconnect();
}
this.layouts.main.layoutCtx.layoutDataChanged.unsubscribe();
this.layouts.right.layoutCtx.layoutDataChanged.unsubscribe();
}
public runChangeDetection() {
@ -588,7 +628,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
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 {
@ -703,7 +743,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.mobileService.onDashboardRightLayoutChanged(this.isRightLayoutOpened);
}
private updateLayoutSizes() {
public updateLayoutSizes() {
let changeMainLayoutSize = false;
let changeRightLayoutSize = false;
if (this.dashboardCtx.state) {
@ -718,6 +758,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
private updateMainLayoutSize(): boolean {
const prevMainLayoutWidth = this.mainLayoutSize.width;
const prevMainLayoutHeight = this.mainLayoutSize.height;
const prevMainLayoutMaxWidth = this.mainLayoutSize.maxWidth;
if (this.isEditingWidget && this.editingLayoutCtx.id === 'main') {
this.mainLayoutSize.width = '100%';
} else {
@ -728,7 +769,31 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} else {
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 {
@ -864,6 +929,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: {
settings: deepClone(this.dashboard.configuration.settings),
gridSettings,
breakpointId: this.layouts.main.layoutCtx.breakpoint
}
}).afterClosed().subscribe((data) => {
if (data) {
@ -1027,7 +1093,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
public saveDashboard() {
this.translatedDashboardTitle = this.getTranslatedDashboardTitle();
this.setEditMode(false, false);
this.notifyDashboardUpdated();
}
@ -1056,21 +1121,15 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.updateLayout(layout, layoutInfo);
} else {
layout.show = false;
this.updateLayout(layout, {widgetIds: [], widgetLayouts: {}, gridSettings: null});
this.updateLayout(layout, {default: {widgetIds: [], widgetLayouts: {}, gridSettings: null}});
}
}
}
private updateLayout(layout: DashboardPageLayout, layoutInfo: DashboardLayoutInfo) {
if (layoutInfo.gridSettings) {
layout.layoutCtx.gridSettings = layoutInfo.gridSettings;
}
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;
layout.layoutCtx.layoutData = layoutInfo;
layout.layoutCtx.layoutDataChanged.next();
layout.layoutCtx.ctrl?.updatedCurrentBreakpoint(this.isEdit ? layout.layoutCtx.breakpoint : null, layout.show);
this.updateLayoutSizes();
}
@ -1145,8 +1204,33 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
data: widget
};
this.window.parent.postMessage(JSON.stringify(message), '*');
this.setEditMode(false, false);
} 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) {
this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget);
this.layouts[layoutId].layoutCtx.widgets.addWidgetId(widget.id);
const layoutCtx = this.layouts[layoutId].layoutCtx;
this.dashboardUtils.addWidgetToLayout(this.dashboard, this.dashboardCtx.state, layoutId, widget, undefined,
undefined, -1, -1, layoutCtx.breakpoint);
layoutCtx.widgets.addWidgetId(widget.id);
this.runChangeDetection();
}
@ -1250,10 +1336,20 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
col: 0
};
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();
if (scada) {
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) {
this.addWidgetToDashboard(newWidget);
} else {
@ -1269,7 +1365,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
stateController: this.dashboardCtx.stateController,
widget: newWidget,
widgetInfo: widgetTypeInfo,
scada
showLayoutConfig,
isDefaultBreakpoint
}
}).afterClosed().subscribe((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) {
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) {
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) {
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(
(widget) => {
layoutCtx.widgets.addWidgetId(widget.id);
@ -1368,7 +1485,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
}
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(
(widget) => {
layoutCtx.widgets.addWidgetId(widget.id);
@ -1390,7 +1507,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
).subscribe((res) => {
if (res) {
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();
}
}
@ -1399,7 +1517,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string) {
$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) {
@ -1455,7 +1573,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
action: ($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',
icon: 'content_paste',
shortcut: 'M-I'
@ -1475,7 +1593,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
return dashboardContextActions;
}
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget): Array<WidgetContextMenuItem> {
prepareWidgetContextMenu(layoutCtx: DashboardPageLayoutContext, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem> {
const widgetContextActions: Array<WidgetContextMenuItem> = [];
if (this.isEdit && !this.isEditingWidget) {
widgetContextActions.push(
@ -1488,6 +1606,18 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
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) {
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.
///
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 { Timewindow } from '@shared/models/time/time.models';
import { IAliasController, IStateController } from '@core/api/widget-api.models';
import { ILayoutController } from './layout/layout.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 { ElementRef } from '@angular/core';
export declare type DashboardPageScope = 'tenant' | 'customer';
@ -35,6 +43,7 @@ export interface DashboardPageInitData {
export interface DashboardContext {
instanceId: string;
state: string;
breakpoint: BreakpointId;
getDashboard: () => Dashboard;
dashboardTimewindow: Timewindow;
aliasController: IAliasController;
@ -46,17 +55,20 @@ export interface DashboardContext {
export interface IDashboardController {
dashboardCtx: DashboardContext;
dashboardContainer: ElementRef;
elRef: ElementRef;
openRightLayout();
openDashboardState(stateId: string, openRightLayout: boolean);
addWidget($event: Event, layoutCtx: DashboardPageLayoutContext);
editWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
replaceReferenceWithWidgetCopy($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
exportWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget, widgetTitle: string);
removeWidget($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
widgetMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
dashboardMouseDown($event: Event, layoutCtx: DashboardPageLayoutContext);
widgetClicked($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
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);
copyWidgetReference($event: Event, layoutCtx: DashboardPageLayoutContext, widget: Widget);
pasteWidget($event: Event, layoutCtx: DashboardPageLayoutContext, pos: WidgetPosition);
@ -65,6 +77,9 @@ export interface IDashboardController {
export interface DashboardPageLayoutContext {
id: DashboardLayoutId;
layoutData: DashboardLayoutInfo;
layoutDataChanged: BehaviorSubject<void>;
breakpoint: BreakpointId;
widgets: LayoutWidgetsArray;
widgetLayouts: WidgetLayouts;
gridSettings: GridSettings;

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

@ -105,18 +105,18 @@
</mat-expansion-panel>
</div>
<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-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 translate>dashboard.columns-count</div>
<mat-form-field *ngIf="!isScada()"
@ -188,6 +188,25 @@
{{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle>
</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 translate>dashboard.background-color</div>
<tb-color-input asBoxInput
@ -212,7 +231,7 @@
</mat-form-field>
</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 *ngIf="isRightLayout" class="tb-form-row">
<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.
///
import { Component, Inject, OnInit, SkipSelf } from '@angular/core';
import { Component, Inject, OnDestroy, OnInit, SkipSelf } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
@ -24,20 +24,26 @@ import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { TranslateService } from '@ngx-translate/core';
import {
BreakpointId,
DashboardSettings,
GridSettings,
LayoutType,
layoutTypes, layoutTypeTranslationMap,
StateControllerId
StateControllerId,
ViewFormatType,
viewFormatTypes,
viewFormatTypeTranslationMap
} from '@app/shared/models/dashboard.models';
import { isDefined, isUndefined } from '@core/utils';
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 {
settings?: DashboardSettings;
gridSettings?: GridSettings;
isRightLayout?: boolean;
breakpointId?: BreakpointId;
}
@Component({
@ -47,11 +53,10 @@ export interface DashboardSettingsDialogData {
styleUrls: ['./dashboard-settings-dialog.component.scss']
})
export class DashboardSettingsDialogComponent extends DialogComponent<DashboardSettingsDialogComponent, DashboardSettingsDialogData>
implements OnInit, ErrorStateMatcher {
implements OnDestroy, ErrorStateMatcher {
layoutTypes = layoutTypes;
layoutTypeTranslations = layoutTypeTranslationMap;
viewFormatTypes = viewFormatTypes;
viewFormatTypeTranslationMap = viewFormatTypeTranslationMap;
settings: DashboardSettings;
gridSettings: GridSettings;
@ -62,10 +67,17 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
stateControllerIds: string[];
layoutSettingsType: string;
submitted = false;
scadaColumns: number[] = [];
private layoutType = LayoutType.default;
private breakpointId: BreakpointId = 'default';
private destroy$ = new Subject<void>();
private stateControllerTranslationMap = new Map<string, string>([
['default', 'dashboard.state-controller-default'],
]);
@ -77,7 +89,8 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
public dialogRef: MatDialogRef<DashboardSettingsDialogComponent, DashboardSettingsDialogData>,
private fb: FormBuilder,
private translate: TranslateService,
private statesControllerService: StatesControllerService) {
private statesControllerService: StatesControllerService,
private dashboardUtils: DashboardUtilsService) {
super(store, router, dialogRef);
this.stateControllerIds = Object.keys(this.statesControllerService.getStateControllers());
@ -85,6 +98,7 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
this.settings = this.data.settings;
this.gridSettings = this.data.gridSettings;
this.isRightLayout = this.data.isRightLayout;
this.breakpointId = this.data.breakpointId;
if (this.settings) {
const showTitle = isUndefined(this.settings.showTitle) ? true : this.settings.showTitle;
@ -116,14 +130,18 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
disabled: hideToolbar}, []],
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) => {
if (stateControllerId !== 'default') {
this.settingsFormGroup.get('toolbarAlwaysOpen').setValue(true);
}
}
);
this.settingsFormGroup.get('showTitle').valueChanges.subscribe(
this.settingsFormGroup.get('showTitle').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(
(showTitleValue: boolean) => {
if (showTitleValue) {
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) => {
if (showDashboardLogoValue) {
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) => {
if (hideToolbarValue) {
this.settingsFormGroup.get('toolbarAlwaysOpen').disable();
@ -167,6 +189,16 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
}
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;
while (columns <= 1008) {
this.scadaColumns.push(columns);
@ -174,14 +206,16 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
}
const mobileAutoFillHeight = isUndefined(this.gridSettings.mobileAutoFillHeight) ? false : this.gridSettings.mobileAutoFillHeight;
this.gridSettingsFormGroup = this.fb.group({
layoutType: [this.gridSettings.layoutType || LayoutType.default, []],
columns: [this.gridSettings.columns || 24, [Validators.required, Validators.min(10), Validators.max(1008)]],
minColumns: [this.gridSettings.minColumns || this.gridSettings.columns || 24,
[Validators.required, Validators.min(10), Validators.max(1008)]],
margin: [isDefined(this.gridSettings.margin) ? this.gridSettings.margin : 10,
[Validators.required, Validators.min(0), Validators.max(50)]],
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, []],
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)', []],
backgroundImageUrl: [this.gridSettings.backgroundImageUrl, []],
backgroundSizeMode: [this.gridSettings.backgroundSizeMode || '100%', []],
@ -194,27 +228,56 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
isUndefined(this.gridSettings.mobileDisplayLayoutFirst) ? false : this.gridSettings.mobileDisplayLayoutFirst;
this.gridSettingsFormGroup.addControl('mobileDisplayLayoutFirst', this.fb.control(mobileDisplayLayoutFirst, []));
}
merge(this.gridSettingsFormGroup.get('layoutType').valueChanges,
this.gridSettingsFormGroup.get('mobileAutoFillHeight').valueChanges).subscribe(
() => {
this.updateGridSettingsFormState();
if (this.isScada()) {
this.gridSettingsFormGroup.get('margin').disable();
this.gridSettingsFormGroup.get('outerMargin').disable();
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);
}
);
this.updateGridSettingsFormState();
} else {
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 {
this.gridSettingsFormGroup = this.fb.group({});
}
}
ngOnInit(): void {
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
isScada() {
if (this.gridSettingsFormGroup) {
return this.gridSettingsFormGroup.get('layoutType').value === LayoutType.scada;
} else {
return false;
}
return this.layoutType === LayoutType.scada;
}
get isDefaultLayout() {
return this.layoutType === LayoutType.default;
}
get isDefaultBreakpoint(): boolean {
return this.breakpointId === 'default';
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
@ -248,29 +311,28 @@ export class DashboardSettingsDialogComponent extends DialogComponent<DashboardS
}
private updateGridSettingsFormState() {
const isScada: boolean = this.gridSettingsFormGroup.get('layoutType').value === LayoutType.scada;
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});
if (this.breakpointId === 'default') {
const mobileAutoFillHeight: boolean = this.gridSettingsFormGroup.get('mobileAutoFillHeight').value;
if (mobileAutoFillHeight) {
this.gridSettingsFormGroup.get('mobileRowHeight').disable();
} else {
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} {
gap: 6px;
&.tb-left-panel {
gap: 3px;
}
}
@media #{$mat-md} {
gap: 6px;
gap: 3px;
&.tb-left-panel {
gap: 1px;
}
}
@media #{$mat-gt-md} {
gap: 12px;
gap: 6px;
&.tb-left-panel {
gap: 3px;
}
}
@media #{$mat-gt-sm} {

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

@ -24,7 +24,8 @@
[dashboard]="dashboard"
[widget]="widget"
[hideToggleHeader]="previewMode"
[scada]="scada"
[showLayoutConfig]="showLayoutConfig"
[isDefaultBreakpoint]="isDefaultBreakpoint"
formControlName="widgetConfig">
<div class="tb-widget-config-header-prefix">
<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()
@coerceBoolean()
scada = false;
showLayoutConfig = true;
@Input()
@coerceBoolean()
isDefaultBreakpoint= true;
@Output()
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"
[autofillHeight]="autoFillHeight"
[mobileAutofillHeight]="mobileAutoFillHeight"
[mobileRowHeight]="layoutCtx.gridSettings.mobileRowHeight"
[isMobile]="isMobile"
[mobileRowHeight]="mobielRowHeigth"
[isMobile]="isMobileValue"
[isMobileDisabled]="isMobileDisabled"
[disableWidgetInteraction]="isEdit"
[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 { map } from 'rxjs/operators';
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({
selector: 'tb-dashboard-layout',
@ -85,11 +86,27 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
}
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 {
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 {
@ -129,11 +146,13 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
private rxSubscriptions = new Array<Subscription>();
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private itembuffer: ItemBufferService,
private imagePipe: ImagePipe,
private sanitizer: DomSanitizer) {
constructor(
protected store: Store<AppState>,
private translate: TranslateService,
private itembuffer: ItemBufferService,
private imagePipe: ImagePipe,
private sanitizer: DomSanitizer,
) {
super(store);
this.initHotKeys();
}
@ -199,7 +218,7 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
new Hotkey('ctrl+i', (event: KeyboardEvent) => {
if (this.isEdit && !this.isEditingWidget && !this.widgetEditMode) {
if (this.itembuffer.canPasteWidgetReference(this.dashboardCtx.getDashboard(),
this.dashboardCtx.state, this.layoutCtx.id)) {
this.dashboardCtx.state, this.layoutCtx.id, this.layoutCtx.breakpoint)) {
event.preventDefault();
this.pasteWidgetReference(event);
}
@ -264,6 +283,10 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
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 {
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);
}
prepareDashboardContextMenu($event: Event): Array<DashboardContextMenuItem> {
prepareDashboardContextMenu(_: Event): Array<DashboardContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareDashboardContextMenu(this.layoutCtx);
}
prepareWidgetContextMenu($event: Event, widget: Widget): Array<WidgetContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget);
prepareWidgetContextMenu(_: Event, widget: Widget, isReference: boolean): Array<WidgetContextMenuItem> {
return this.layoutCtx.dashboardCtrl.prepareWidgetContextMenu(this.layoutCtx, widget, isReference);
}
copyWidget($event: Event, widget: Widget) {
@ -310,4 +333,24 @@ export class DashboardLayoutComponent extends PageComponent implements ILayoutCo
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);
pasteWidget($event: MouseEvent);
pasteWidgetReference($event: MouseEvent);
updatedCurrentBreakpoint(breakpoint?: string, showLayout?: boolean);
}
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>
</button>
</mat-toolbar>
<div mat-dialog-content fxLayout="column" fxLayoutGap="20px" style="min-width: 300px; overflow: hidden;">
<div fxLayout="row" fxLayoutAlign="start center">
<mat-slide-toggle formControlName="right" color="accent">
{{ 'layout.divider' | translate }}
</mat-slide-toggle>
</div>
<div mat-dialog-content>
<section class="tb-form-panel no-padding no-border">
<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 *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">
<mat-button-toggle-group aria-label="Select width value type"
formControlName="type"
[fxShow]="layoutsFormGroup.get('right').value">
[fxShow]="isDividerLayout">
<mat-button-toggle fxFlex value="percentage">
{{ 'layout.percentage-width' | translate }}
</mat-button-toggle>
@ -52,7 +113,7 @@
<div fxLayout="column" fxLayoutAlign="start center" fxFlex="{{ buttonFlexValue() }}" class="tb-layout-preview-container-main">
<button mat-icon-button
type="button"
matTooltip="{{ 'dashboard.layout-settings' | translate }}"
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('main')"
class="tb-layout-preview-element"
@ -72,7 +133,7 @@
<span [matTooltip]="layoutButtonText('main')"
#mainLayoutTooltip="matTooltip"
matTooltipPosition="above">
{{ (layoutsFormGroup.value.right ? 'layout.left' : 'layout.main') | translate }}
{{ (isDividerLayout ? 'layout.left' : 'layout.main') | translate }}
</span>
</button>
<div fxFlex class="tb-layout-preview-element tb-layout-preview-input" *ngIf="showPreviewInputs('main')">
@ -93,10 +154,10 @@
required>
</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
type="button"
matTooltip="{{ 'dashboard.layout-settings' | translate }}"
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('right')"
class="tb-layout-preview-element"
@ -138,7 +199,7 @@
</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"
step="1"
min="{{ layoutPercentageSize.MIN }}"
@ -167,6 +228,7 @@
</div>
</div>
</div>
</ng-template>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">
<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;
@import '../theme.scss';
@import '../scss/constants.scss';
$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 {
width: 120%;
width: 100%;
background-color: rgba(mat.get-color-from-palette($tb-primary, 50), 0.6);
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 {
@ -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 {
/* Alarm tooltip with side-to-side movement */
.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.
///
import { Component, Inject, SkipSelf, ViewChild } from '@angular/core';
import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
AbstractControl,
FormGroupDirective,
NgForm,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
FormGroupDirective,
NgForm,
Validators
} from '@angular/forms';
import { Router } from '@angular/router';
import { DialogComponent } from '@app/shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { TranslateService } from '@ngx-translate/core';
import { DashboardLayoutId, DashboardStateLayouts, LayoutDimension } from '@app/shared/models/dashboard.models';
import { deepClone, isDefined } from '@core/utils';
import {
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 {
DashboardSettingsDialogComponent,
@ -46,11 +55,25 @@ import {
} from '@home/components/dashboard-page/layout/layout.models';
import { Subscription } from 'rxjs';
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 {
layouts: DashboardStateLayouts;
}
export interface DashboardLayoutSettings {
icon: string;
name: string;
descriptionSize?: string;
layout: DashboardLayout;
breakpoint: BreakpointId;
}
@Component({
selector: 'tb-manage-dashboard-layouts-dialog',
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']
})
export class ManageDashboardLayoutsDialogComponent extends DialogComponent<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>
implements ErrorStateMatcher {
implements ErrorStateMatcher, OnDestroy {
@ViewChild('tooltip', {static: true}) tooltip: MatTooltip;
@ViewChild('tooltip') tooltip: MatTooltip;
layoutsFormGroup: UntypedFormGroup;
@ -70,29 +93,43 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutFixedSize = LayoutFixedSize;
layoutTypes = layoutTypes;
layoutTypeTranslations = layoutTypeTranslationMap;
layoutBreakpoints: DashboardLayoutSettings[] = [];
private readonly layouts: DashboardStateLayouts;
private subscriptions: Array<Subscription> = [];
private submitted = false;
allowBreakpointIds: BreakpointId[] = [];
selectedBreakpointIds: BreakpointId[] = ['default'];
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) private data: ManageDashboardLayoutsDialogData,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
protected dialogRef: MatDialogRef<ManageDashboardLayoutsDialogComponent, DashboardStateLayouts>,
private fb: UntypedFormBuilder,
private utils: UtilsService,
private dashboardUtils: DashboardUtilsService,
private translate: TranslateService,
private dialog: MatDialog) {
private dialog: MatDialog,
private dialogs: DialogService) {
super(store, router, dialogRef);
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({
layoutType: [layoutType],
main: [{value: isDefined(this.layouts.main), disabled: true}],
right: [isDefined(this.layouts.right)],
sliderPercentage: [50],
sliderFixed: [this.layoutFixedSize.MIN],
leftWidthPercentage: [50,
@ -141,7 +178,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
sliderFixed: this.layouts.main.gridSettings.layoutDimension.fixedWidth
}, {emitEvent: false});
} else {
const leftWidthPercentage: number = Number(this.layouts.main.gridSettings.layoutDimension.leftWidthPercentage);
const leftWidthPercentage = Number(this.layouts.main.gridSettings.layoutDimension.leftWidthPercentage);
this.layoutsFormGroup.patchValue({
leftWidthPercentage,
sliderPercentage: leftWidthPercentage,
@ -158,6 +195,21 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
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.layoutsFormGroup.get('sliderPercentage').valueChanges
.subscribe(
@ -209,8 +261,10 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
return originalErrorState || customErrorState;
}
openLayoutSettings(layoutId: DashboardLayoutId) {
const gridSettings = deepClone(this.layouts[layoutId].gridSettings);
openLayoutSettings(layoutId: DashboardLayoutId, breakpointId: BreakpointId = 'default') {
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,
DashboardSettingsDialogData>(DashboardSettingsDialogComponent, {
disableClose: true,
@ -218,11 +272,12 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
data: {
settings: null,
gridSettings,
isRightLayout: this.layoutsFormGroup.get('right').value && layoutId === 'right'
isRightLayout: this.isDividerLayout && layoutId === 'right',
breakpointId
}
}).afterClosed().subscribe((data) => {
if (data && data.gridSettings) {
this.dashboardUtils.updateLayoutSettings(this.layouts[layoutId], data.gridSettings);
this.dashboardUtils.updateLayoutSettings(layout, data.gridSettings);
this.layoutsFormGroup.markAsDirty();
}
});
@ -234,20 +289,24 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
save(): void {
this.submitted = true;
const layouts = ['main', 'right'];
for (const l of layouts) {
const control = this.layoutsFormGroup.controls[l];
if (!control.value) {
if (this.layouts[l]) {
delete this.layouts[l];
const layoutType = this.layoutsFormGroup.value.layoutType;
this.layouts.main.gridSettings.layoutType = layoutType;
if (!this.isDividerLayout) {
delete this.layouts.right;
if (this.layouts.main.breakpoints) {
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;
if (this.layouts.right?.gridSettings) {
delete this.layouts.right.gridSettings.layoutDimension;
}
if (this.layoutsFormGroup.value.right) {
if (this.isDividerLayout) {
const formValues = this.layoutsFormGroup.value;
const widthType = formValues.type;
const layoutDimension: LayoutDimension = {
@ -271,7 +330,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
buttonFlexValue(): number {
const formValues = this.layoutsFormGroup.value;
if (formValues.right) {
if (this.isDividerLayout) {
if (formValues.type !== LayoutWidthType.FIXED) {
return formValues.leftWidthPercentage;
} else {
@ -288,7 +347,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
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);
this.layoutsFormGroup.get(key).setValue(valueToSet, {emitEvent: false});
this.layoutsFormGroup.get('sliderPercentage')
@ -296,7 +355,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
}
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').markAsDirty();
}
@ -339,7 +398,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutButtonClass(side: DashboardLayoutId, border: boolean = false): string {
const formValues = this.layoutsFormGroup.value;
if (formValues.right) {
if (this.isDividerLayout) {
let classString = border ? 'tb-layout-button-main ' : '';
if (!(formValues.fixedLayout === side || formValues.type === LayoutWidthType.PERCENTAGE)) {
classString += 'tb-fixed-layout-button';
@ -350,7 +409,7 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
layoutButtonText(side: DashboardLayoutId): string {
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') {
return this.translate.instant('layout.left-side');
} else {
@ -361,6 +420,116 @@ export class ManageDashboardLayoutsDialogComponent extends DialogComponent<Manag
showPreviewInputs(side: DashboardLayoutId): boolean {
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"
(click)="item.action(widgetContextMenuEvent, widget)">
<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>
</button>
</div>

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

@ -17,7 +17,6 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DoCheck,
Input,
@ -46,7 +45,6 @@ import {
} from '../../models/dashboard-component.models';
import { ReplaySubject, Subject, Subscription } from 'rxjs';
import { WidgetLayout, WidgetLayouts } from '@shared/models/dashboard.models';
import { DialogService } from '@core/services/dialog.service';
import { animatedScroll, deepClone, isDefined } from '@app/core/utils';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
@ -203,9 +201,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widgetContextMenuEvent: MouseEvent;
dashboardWidgets = new DashboardWidgets(this,
this.differs.find([]).create<Widget>((index, item) => {
return item;
}),
this.differs.find([]).create<Widget>((_, item) => item),
this.kvDiffers.find([]).create<string, WidgetLayout>()
);
@ -218,11 +214,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
constructor(protected store: Store<AppState>,
public utils: UtilsService,
private timeService: TimeService,
private dialogService: DialogService,
private breakpointObserver: BreakpointObserver,
private differs: IterableDiffers,
private kvDiffers: KeyValueDiffers,
private cd: ChangeDetectorRef,
private ngZone: NgZone) {
super(store);
this.authUser = getCurrentAuthUser(store);
@ -257,8 +251,8 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
displayGrid: this.displayGrid,
resizable: {enabled: this.isEdit && !this.isEditingWidget, delayStart: 50},
draggable: {enabled: this.isEdit && !this.isEditingWidget},
itemChangeCallback: item => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (item, itemComponent) => {
itemChangeCallback: () => this.dashboardWidgets.sortWidgets(),
itemInitCallback: (_, itemComponent) => {
(itemComponent.item as DashboardWidget).gridsterItemComponent = itemComponent;
},
colWidthUpdateCallback: (colWidth) => {
@ -421,7 +415,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
private openWidgetContextMenu($event: MouseEvent, widget: DashboardWidget) {
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) {
$event.preventDefault();
$event.stopPropagation();
@ -459,6 +453,9 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
case WidgetComponentActionType.REMOVE:
this.removeWidget($event, widget);
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) {
if ($event) {
$event.stopPropagation();
@ -558,7 +564,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
widget.gridsterItemComponent$().subscribe((gridsterItem) => {
const gridsterItemElement = gridsterItem.el as HTMLElement;
const offset = (parentElement.clientHeight - gridsterItemElement.clientHeight) / 2;
let scrollTop;
let scrollTop: number;
if (this.isMobileSize) {
scrollTop = gridsterItemElement.offsetTop;
} else {
@ -592,8 +598,7 @@ export class DashboardComponent extends PageComponent implements IDashboardCompo
} else {
this.gridsterOpts.gridType = this.isMobileSize ? GridType.Fixed : this.gridType || GridType.ScrollVertical;
}
const mobileBreakPoint = this.isMobileSize ? 20000 : 0;
this.gridsterOpts.mobileBreakpoint = mobileBreakPoint;
this.gridsterOpts.mobileBreakpoint = this.isMobileSize ? 20000 : 0;
const rowSize = this.detectRowSize(this.isMobileSize, autofillHeight, parentHeight);
if (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();
}
updateData(closeDetails: boolean = true) {
updateData(closeDetails: boolean = true, reloadEntity: boolean = true) {
if (closeDetails) {
this.isDetailsOpen = false;
}
@ -427,7 +427,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
timePageLink.endTime = interval.endTime;
}
this.dataSource.loadEntities(this.pageLink);
if (this.isDetailsOpen && this.entityDetailsPanel) {
if (reloadEntity && this.isDetailsOpen && this.entityDetailsPanel) {
this.entityDetailsPanel.reloadEntity();
}
}
@ -511,7 +511,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
}
onEntityUpdated(entity: BaseData<HasId>) {
this.updateData(false);
this.updateData(false, false);
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 { TbAnchorComponent } from '@shared/components/tb-anchor.component';
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 { EntityTabsComponent } from '@home/components/entity/entity-tabs.component';
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({
selector: 'tb-entity-details-panel',
@ -288,7 +289,16 @@ export class EntityDetailsPanelComponent extends PageComponent implements AfterV
editingEntity.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) => {
this.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 { AddWidgetDialogComponent } from '@home/components/dashboard-page/add-widget-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 { ManageDashboardStatesDialogComponent } from '@home/components/dashboard-page/states/manage-dashboard-states-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 { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delete-timeseries-panel.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({
declarations:
@ -285,11 +291,13 @@ import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layo
DashboardPageComponent,
DashboardStateComponent,
DashboardLayoutComponent,
SelectDashboardBreakpointComponent,
EditWidgetComponent,
DashboardWidgetSelectComponent,
AddWidgetDialogComponent,
MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent,
AddNewBreakpointDialogComponent,
DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent,
DashboardStateDialogComponent,
@ -418,11 +426,13 @@ import { MoveWidgetsDialogComponent } from '@home/components/dashboard-page/layo
DashboardPageComponent,
DashboardStateComponent,
DashboardLayoutComponent,
SelectDashboardBreakpointComponent,
EditWidgetComponent,
DashboardWidgetSelectComponent,
AddWidgetDialogComponent,
MoveWidgetsDialogComponent,
ManageDashboardLayoutsDialogComponent,
AddNewBreakpointDialogComponent,
DashboardSettingsDialogComponent,
ManageDashboardStatesDialogComponent,
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) {
const rootPath = sectionPath.substring(0, sectionPath.length - found.path.length);
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 [];

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 { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract';
@Component({
@ -78,7 +77,7 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs
}
],
standalone: true,
imports: [CommonModule, SharedModule, TruncateWithTooltipDirective]
imports: [CommonModule, SharedModule]
})
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 { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-modbus-data-keys-panel',
@ -52,7 +51,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
CommonModule,
SharedModule,
GatewayHelpLinkPipe,
TruncateWithTooltipDirective,
]
})
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 { takeUntil } from 'rxjs/operators';
import { coerceBoolean } from '@shared/decorators/coercion';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-modbus-security-config',
@ -64,7 +63,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
imports: [
CommonModule,
SharedModule,
TruncateWithTooltipDirective,
]
})
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 { ModbusValuesComponent, } from '../modbus-values/modbus-values.component';
import { isEqual } from '@core/utils';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-modbus-slave-config',
@ -73,7 +72,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
TruncateWithTooltipDirective,
],
})
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 { takeUntil } from 'rxjs/operators';
import { isEqual } from '@core/utils';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import { helpBaseUrl } from '@shared/models/constants';
@Component({
@ -78,7 +77,6 @@ import { helpBaseUrl } from '@shared/models/constants';
ModbusValuesComponent,
ModbusSecurityConfigComponent,
GatewayPortTooltipPipe,
TruncateWithTooltipDirective,
],
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>
</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="fixed-title-width" tb-hint-tooltip-icon="{{ 'gateway.hints.sub-check-period' | translate }}">
<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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
import {
SecurityConfigComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/security-config/security-config.component';
import { HOUR } from '@shared/models/time/time.models';
@Component({
selector: 'tb-opc-server-config',
@ -62,7 +62,6 @@ import {
CommonModule,
SharedModule,
SecurityConfigComponent,
TruncateWithTooltipDirective,
]
})
export class OpcServerConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@ -79,7 +78,8 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
this.serverConfigFormGroup = this.fb.group({
url: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
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, []],
subCheckPeriodInMillis: [100, [Validators.required, Validators.min(100)]],
showMap: [false, []],
@ -117,7 +117,8 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
writeValue(serverConfig: ServerConfig): void {
const {
timeoutInMillis = 1000,
scanPeriodInMillis = 1000,
scanPeriodInMillis = HOUR,
pollPeriodInMillis = 5000,
enableSubscriptions = true,
subCheckPeriodInMillis = 100,
showMap = false,
@ -129,6 +130,7 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
...serverConfig,
timeoutInMillis,
scanPeriodInMillis,
pollPeriodInMillis,
enableSubscriptions,
subCheckPeriodInMillis,
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 { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-workers-config-control',
@ -45,7 +44,6 @@ import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-t
imports: [
CommonModule,
SharedModule,
TruncateWithTooltipDirective,
],
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')">
{{ 'gateway.min-pack-send-delay-min' | translate }}
</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;"
matTooltip="{{ 'gateway.hints.minimal-pack-delay' | translate }}">info_outlined
</mat-icon>
@ -747,22 +751,64 @@
</mat-icon>
</mat-form-field>
</section>
<mat-form-field appearance="outline" class="mat-block">
<mat-label translate>gateway.statistics.check-connectors-configuration</mat-label>
<input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')">
{{ 'gateway.statistics.check-connectors-configuration-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')">
{{ 'gateway.statistics.check-connectors-configuration-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')">
{{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }}
</mat-error>
</mat-form-field>
<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.check-connectors-configuration</mat-label>
<input matInput formControlName="checkConnectorsConfigurationInSeconds" type="number" min="0"/>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('required')">
{{ 'gateway.statistics.check-connectors-configuration-required' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('min')">
{{ 'gateway.statistics.check-connectors-configuration-min' | translate }}
</mat-error>
<mat-error
*ngIf="gatewayConfigGroup.get('thingsboard.checkConnectorsConfigurationInSeconds').hasError('pattern')">
{{ 'gateway.statistics.check-connectors-configuration-pattern' | translate }}
</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>
</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]+$/)]],
commands: this.fb.array([], [])
}),
maxPayloadSizeBytes: [1024, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [200, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
maxPayloadSizeBytes: [8196, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [50, [Validators.required, Validators.min(10), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({
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;
timeoutInMillis: number;
scanPeriodInMillis: number;
pollPeriodInMillis: number;
enableSubscriptions: boolean;
subCheckPeriodInMillis: number;
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 { emptyPageData, PageData } from '@shared/models/page/page-data';
import { deepClone } from '@core/utils';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
@Component({
selector: 'tb-quick-link',
@ -114,6 +115,7 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control
private fb: UntypedFormBuilder,
private menuService: MenuService,
public translate: TranslateService,
private customTranslate: CustomTranslatePipe,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher) {
super(store);
}
@ -135,7 +137,8 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control
this.updateView(modelValue);
}),
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(),
switchMap(name => this.fetchLinks(name) ),
share()
@ -202,7 +205,8 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control
}
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>> {
this.searchText = searchText;
@ -228,7 +232,8 @@ export class QuickLinkComponent extends PageComponent implements OnInit, Control
map((links) => {
const result = deepClone(links);
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;
}
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)">
<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>
<span translate>{{place.name}}</span>
<span>{{place.customTranslate ? (place.name | customTranslate) : (place.name | translate)}}</span>
</a>
</mat-grid-tile>
</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 { AppState } from '@core/core.state';
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 { map } from 'rxjs/operators';
@ -38,9 +38,7 @@ export class NavigationCardsWidgetComponent extends PageComponent implements OnI
homeSections$ = this.menuService.homeSections();
showHomeSections$ = this.homeSections$.pipe(
map((sections) => {
return sections.filter((section) => this.sectionPlaces(section).length > 0);
})
map((sections) => sections.filter((section) => this.sectionPlaces(section).length > 0))
);
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)) : [];
}
private filterPlace(place: HomeSectionPlace): boolean {
private filterPlace(place: MenuSection): boolean {
if (this.settings.filterType === 'include') {
return this.settings.filter.includes(place.path);
} 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 #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">
<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 }}
</mat-slide-toggle>
<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,
basicModeDirective: this.utils.editWidgetInfo.basicModeDirective,
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>();

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

@ -141,9 +141,8 @@ import {
import {
TypeValuePanelComponent
} 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 {
ModbusRpcParametersComponent
ModbusRpcParametersComponent
} 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';
@ -233,7 +232,6 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent,
EllipsisChipListDirective,
TruncateWithTooltipDirective,
ModbusRpcParametersComponent,
],
exports: [

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

@ -169,12 +169,12 @@
</div>
</div>
<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">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel">
<div class="tb-form-panel" *ngIf="isDefaultBreakpoint">
<mat-slide-toggle class="mat-slide" formControlName="desktopHide">
{{ 'widget-config.desktop-hide' | translate }}
</mat-slide-toggle>

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

@ -38,7 +38,7 @@ import {
JsonSchema,
JsonSettingsSchema,
TargetDevice,
TargetDeviceType, targetDeviceValid,
targetDeviceValid,
Widget,
WidgetConfigMode,
widgetType
@ -84,8 +84,8 @@ import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { coerceBoolean } from '@shared/decorators/coercion';
import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module';
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 Timeout = NodeJS.Timeout;
const emptySettingsSchema: JsonSchema = {
type: 'object',
@ -152,7 +152,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
@Input()
@coerceBoolean()
scada = false;
showLayoutConfig = true;
@Input()
@coerceBoolean()
isDefaultBreakpoint = true;
@Input() disabled: boolean;
@ -345,10 +349,12 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
value: 'actions'
}
);
if (!this.scada) {
if (this.showLayoutConfig) {
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'
}
);

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
* limitations under the License.
*/
@import '../scss/constants';
.tb-widget-container {
position: absolute;
inset: 0;
@ -182,12 +184,30 @@ gridster-item:hover {
line-height: 12px;
font-weight: 500;
color: rgba(0, 0, 0, 0.76);
.tb-widget-actions-panel {
.tb-widget-action-container {
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 8px;
flex-direction: column;
gap: 6px;
.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 ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import { from } from 'rxjs';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
export enum WidgetComponentActionType {
MOUSE_DOWN,
@ -47,7 +48,8 @@ export enum WidgetComponentActionType {
CONTEXT_MENU,
EDIT,
EXPORT,
REMOVE
REMOVE,
REPLACE_REFERENCE_WITH_WIDGET_COPY,
}
export class WidgetComponentAction {
@ -117,6 +119,7 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
widgetComponentAction: EventEmitter<WidgetComponentAction> = new EventEmitter<WidgetComponentAction>();
hovered = false;
isReferenceWidget = false;
get widgetEditActionsEnabled(): boolean {
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 renderer: Renderer2,
private container: ViewContainerRef,
private dashboardUtils: DashboardUtilsService,
private utils: UtilsService) {
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('click', (e) => this.onClicked(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 {
@ -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) {
this.widgetComponentAction.emit({
event,
@ -240,9 +254,10 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}
}
private initEditWidgetActionTooltip() {
private initEditWidgetActionTooltip(parent: HTMLElement) {
from(import('tooltipster')).subscribe(() => {
$(this.gridsterItem.el).tooltipster({
parent: $(parent),
delay: this.widget.selected ? [0, 10000000] : [0, 100],
distance: 2,
zIndex: 151,
@ -262,7 +277,9 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
content: '',
functionPosition: (instance, helper, position) => {
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;
return position;
},
@ -280,6 +297,11 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
functionAfter: () => {
this.hovered = false;
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');
@ -327,29 +349,43 @@ export class WidgetContainerComponent extends PageComponent implements OnInit, O
}
@Component({
template: `<div class="tb-widget-actions-panel">
<button mat-icon-button class="tb-mat-20"
[fxShow]="container.isEditActionEnabled"
(click)="container.onEdit($event)"
matTooltip="{{ 'widget.edit' | translate }}"
matTooltipPosition="above">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button class="tb-mat-20"
[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>`,
template: `
<div class="tb-widget-action-container">
<div class="tb-widget-reference-panel tb-primary-fill" *ngIf="container.widget.isReference">
{{ 'widget.reference' | translate }}
<button mat-icon-button class="tb-mat-16"
color="primary"
[fxShow]="container.isEditActionEnabled"
(click)="container.onReplaceReferenceWithWidgetCopy($event)"
matTooltip="{{ 'widget.replace-reference-with-widget-copy' | translate }}"
matTooltipPosition="above">
<tb-icon matButtonIcon>mdi:file-replace-outline</tb-icon>
</button>
</div>
<div class="tb-widget-actions-panel">
<button mat-icon-button class="tb-mat-20"
[fxShow]="container.isEditActionEnabled"
(click)="container.onEdit($event)"
matTooltip="{{ 'widget.edit' | translate }}"
matTooltipPosition="above">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button class="tb-mat-20"
[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: [],
encapsulation: ViewEncapsulation.None
})
@ -361,7 +397,8 @@ export class EditWidgetActionsTooltipComponent implements AfterViewInit {
@Output()
viewInited = new EventEmitter();
constructor(public element: ElementRef<HTMLElement>) {
constructor(public element: ElementRef<HTMLElement>,
public cd: ChangeDetectorRef) {
}
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}}">
<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>

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)">
<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"
[ngClass]="{'tb-toggled' : section.opened}"></span>
</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 {
onEditWidget?: ($event: Event, widget: Widget) => void;
replaceReferenceWithWidgetCopy?: ($event: Event, widget: Widget) => void;
onExportWidget?: ($event: Event, widget: Widget, widgeTitle: string) => void;
onRemoveWidget?: ($event: Event, widget: Widget) => void;
onWidgetMouseDown?: ($event: Event, widget: Widget) => void;
onDashboardMouseDown?: ($event: Event) => void;
onWidgetClicked?: ($event: Event, widget: Widget) => void;
prepareDashboardContextMenu?: ($event: Event) => Array<DashboardContextMenuItem>;
prepareWidgetContextMenu?: ($event: Event, widget: Widget) => Array<WidgetContextMenuItem>;
prepareWidgetContextMenu?: ($event: Event, widget: Widget, isReference: boolean) => Array<WidgetContextMenuItem>;
}
export interface IDashboardComponent {
@ -343,6 +344,7 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget {
private selectedCallback: (selected: boolean) => void = () => {};
isFullscreen = false;
isReference = false;
color: 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;
deviceService: DeviceService;
assetService: AssetService;
@ -665,7 +669,7 @@ export const detailsToWidgetInfo = (widgetTypeDetailsEntity: WidgetTypeDetails):
};
export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
createdTime: number): WidgetType => {
createdTime: number, version: number): WidgetType => {
const descriptor: WidgetTypeDescriptor = {
type: widgetInfo.type,
sizeX: widgetInfo.sizeX,
@ -688,6 +692,7 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
id,
tenantId,
createdTime,
version,
fqn: widgetTypeFqn(widgetInfo.fullFqn),
name: widgetInfo.widgetName,
deprecated: widgetInfo.deprecated,
@ -697,8 +702,8 @@ export const toWidgetType = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
};
export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: TenantId,
createdTime: number): WidgetTypeDetails => {
const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime);
createdTime: number, version: number): WidgetTypeDetails => {
const widgetTypeEntity = toWidgetType(widgetInfo, id, tenantId, createdTime, version);
return {
...widgetTypeEntity,
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 { ImageService } from '@core/http/image.service';
import { ScadaSymbolData } from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { MenuId } from '@core/services/menu.models';
@Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -79,8 +80,7 @@ const routes: Routes = [
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: {
label: 'admin.resources',
icon: 'folder'
menuId: MenuId.resources
}
},
children: [
@ -97,8 +97,7 @@ const routes: Routes = [
path: 'images',
data: {
breadcrumb: {
label: 'image.gallery',
icon: 'filter'
menuId: MenuId.images
}
},
children: [
@ -117,8 +116,7 @@ const routes: Routes = [
path: 'scada-symbols',
data: {
breadcrumb: {
label: 'scada.symbols',
icon: 'view_in_ar'
menuId: MenuId.scada_symbols
}
},
children: [
@ -153,8 +151,7 @@ const routes: Routes = [
path: 'resources-library',
data: {
breadcrumb: {
label: 'resource.resources-library',
icon: 'mdi:rhombus-split'
menuId: MenuId.resources_library
}
},
children: [
@ -196,8 +193,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
showMainLoadingBar: false,
breadcrumb: {
label: 'admin.settings',
icon: 'settings'
menuId: MenuId.settings
}
},
children: [
@ -220,8 +216,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN],
title: 'admin.general-settings',
breadcrumb: {
label: 'admin.general',
icon: 'settings_applications'
menuId: MenuId.general
}
}
},
@ -233,8 +228,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN],
title: 'admin.outgoing-mail-settings',
breadcrumb: {
label: 'admin.outgoing-mail',
icon: 'mail'
menuId: MenuId.mail_server
}
}
},
@ -246,8 +240,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
title: 'admin.notifications-settings',
breadcrumb: {
label: 'admin.notifications',
icon: 'mdi:message-badge'
menuId: MenuId.notification_settings
}
}
},
@ -255,8 +248,7 @@ const routes: Routes = [
path: 'queues',
data: {
breadcrumb: {
label: 'admin.queues',
icon: 'swap_calls'
menuId: MenuId.queues
}
},
children: [
@ -297,8 +289,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN],
title: 'admin.home-settings',
breadcrumb: {
label: 'admin.home',
icon: 'settings_applications'
menuId: MenuId.home_settings
}
}
},
@ -310,8 +301,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN],
title: 'admin.repository-settings',
breadcrumb: {
label: 'admin.repository',
icon: 'manage_history'
menuId: MenuId.repository_settings
}
}
},
@ -323,8 +313,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN],
title: 'admin.auto-commit-settings',
breadcrumb: {
label: 'admin.auto-commit',
icon: 'settings_backup_restore'
menuId: MenuId.auto_commit_settings
}
}
},
@ -336,8 +325,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN],
title: 'admin.mobile-app.mobile-app',
breadcrumb: {
label: 'admin.mobile-app.mobile-app',
icon: 'smartphone'
menuId: MenuId.mobile_app_settings
}
}
},
@ -373,8 +361,7 @@ const routes: Routes = [
data: {
auth: [Authority.SYS_ADMIN, Authority.TENANT_ADMIN],
breadcrumb: {
label: 'security.security',
icon: 'security'
menuId: MenuId.security_settings
}
},
children: [
@ -397,8 +384,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN],
title: 'admin.general',
breadcrumb: {
label: 'admin.general',
icon: 'settings_applications'
menuId: MenuId.security_settings_general
}
}
},
@ -410,8 +396,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN],
title: 'admin.2fa.2fa',
breadcrumb: {
label: 'admin.2fa.2fa',
icon: 'mdi:two-factor-authentication'
menuId: MenuId.two_fa
}
}
},
@ -423,8 +408,7 @@ const routes: Routes = [
auth: [Authority.SYS_ADMIN],
title: 'admin.oauth2.oauth2',
breadcrumb: {
label: 'admin.oauth2.oauth2',
icon: 'mdi:shield-account'
menuId: MenuId.oauth2
}
},
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 { AlarmTableComponent } from '@home/components/alarm/alarm-table.component';
import { AlarmsMode } from '@shared/models/alarm.models';
import { MenuId } from '@core/services/menu.models';
@Injectable()
export class OAuth2LoginProcessingUrlResolver implements Resolve<string> {
@ -41,8 +42,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER],
title: 'alarm.alarms',
breadcrumb: {
label: 'alarm.alarms',
icon: 'mdi:alert-outline'
menuId: MenuId.alarms
},
isPage: true,
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 { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { MenuId } from '@core/services/menu.models';
const apiUsageDashboardJson = '/assets/dashboard/api_usage.json';
@ -38,8 +39,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN],
title: 'api-usage.api-usage',
breadcrumb: {
label: 'api-usage.api-usage',
icon: 'insert_chart'
menuId: MenuId.api_usage
}
},
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 { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { AssetProfilesTableConfigResolver } from './asset-profiles-table-config.resolver';
import { MenuId } from '@core/services/menu.models';
export const assetProfilesRoutes: Routes = [
{
path: 'assetProfiles',
data: {
breadcrumb: {
label: 'asset-profile.asset-profiles',
icon: 'mdi:alpha-a-box'
menuId: MenuId.asset_profiles
}
},
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 { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { MenuId } from '@core/services/menu.models';
export const assetRoutes: Routes = [
{
path: 'assets',
data: {
breadcrumb: {
label: 'asset.assets',
icon: 'domain'
menuId: MenuId.assets
}
},
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 { Authority } from '@shared/models/authority.enum';
import { AuditLogTableComponent } from '@home/components/audit-log/audit-log-table.component';
import { MenuId } from '@core/services/menu.models';
export const auditLogsRoutes: Routes = [
{
@ -27,8 +28,7 @@ export const auditLogsRoutes: Routes = [
auth: [Authority.TENANT_ADMIN],
title: 'audit-log.audit-logs',
breadcrumb: {
label: 'audit-log.audit-logs',
icon: 'track_changes'
menuId: MenuId.audit_log
},
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 { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { MenuId } from '@core/services/menu.models';
const routes: Routes = [
{
path: 'customers',
data: {
breadcrumb: {
label: 'customer.customers',
icon: 'supervisor_account'
menuId: MenuId.customers
}
},
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 { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { ConfirmOnExitGuard } from '@core/guards/confirm-on-exit.guard';
import { MenuId } from '@core/services/menu.models';
@Injectable()
export class DashboardResolver implements Resolve<Dashboard> {
@ -66,8 +67,7 @@ const routes: Routes = [
path: 'dashboards',
data: {
breadcrumb: {
label: 'dashboard.dashboards',
icon: 'dashboard'
menuId: MenuId.dashboards
}
},
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 { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { MenuId } from '@core/services/menu.models';
export const deviceProfilesRoutes: Routes = [
{
path: 'deviceProfiles',
data: {
breadcrumb: {
label: 'device-profile.device-profiles',
icon: 'mdi:alpha-d-box'
menuId: MenuId.device_profiles
}
},
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 { assetRoutes } from '@home/pages/asset/asset-routing.module';
import { entityViewRoutes } from '@home/pages/entity-view/entity-view-routing.module';
import { MenuId } from '@core/services/menu.models';
export const deviceRoutes: Routes = [
{
path: 'devices',
data: {
breadcrumb: {
label: 'device.devices',
icon: 'devices_other'
menuId: MenuId.devices
}
},
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';
import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component';
import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { MenuId } from '@core/services/menu.models';
const routes: Routes = [
{
path: 'edgeManagement',
data: {
breadcrumb: {
label: 'edge.management',
icon: 'settings_input_antenna'
menuId: MenuId.edge_management
}
},
children: [
@ -64,8 +64,7 @@ const routes: Routes = [
path: 'instances',
data: {
breadcrumb: {
label: 'edge.instances',
icon: 'router'
menuId: MenuId.edges
}
},
children: [
@ -307,8 +306,7 @@ const routes: Routes = [
path: 'ruleChains',
data: {
breadcrumb: {
label: 'edge.rulechain-templates',
icon: 'settings_ethernet'
menuId: MenuId.rulechain_templates
}
},
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 { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models';
import { BreadCrumbConfig } from '@shared/components/breadcrumb';
import { MenuId } from '@core/services/menu.models';
export const entityViewRoutes: Routes = [
{
path: 'entityViews',
data: {
breadcrumb: {
label: 'entity-view.entity-views',
icon: 'view_quilt'
menuId: MenuId.entity_views
}
},
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 { otaUpdatesRoutes } from '@home/pages/ota-update/ota-update-routing.module';
import { vcRoutes } from '@home/pages/vc/vc-routing.module';
import { MenuId } from '@core/services/menu.models';
const routes: Routes = [
{
@ -26,8 +27,7 @@ const routes: Routes = [
data: {
auth: [Authority.TENANT_ADMIN],
breadcrumb: {
label: 'feature.advanced-features',
icon: 'construction'
menuId: MenuId.features
}
},
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 { ResourcesService } from '@core/services/resources.service';
import { isDefinedAndNotNull } from '@core/utils';
import { MenuId } from '@core/services/menu.models';
const sysAdminHomePageJson = '/assets/dashboard/sys_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],
title: 'home.home',
breadcrumb: {
label: 'home.home',
icon: 'home'
menuId: MenuId.home
}
},
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">
<a mat-raised-button color="primary" class="tb-card-button" routerLink="{{place.path}}">
<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>
</mat-grid-tile>
</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 { RuleTableConfigResolver } from '@home/pages/notification/rule/rule-table-config.resolver';
import { SendNotificationButtonComponent } from '@home/components/notification/send-notification-button.component';
import { MenuId } from '@core/services/menu.models';
const routes: Routes = [
{
@ -33,8 +34,7 @@ const routes: Routes = [
data: {
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN],
breadcrumb: {
label: 'notification.notification-center',
icon: 'mdi:message-badge'
menuId: MenuId.notifications_center
},
routerTabsHeaderComponent: SendNotificationButtonComponent
},
@ -54,8 +54,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER, Authority.SYS_ADMIN],
title: 'notification.inbox',
breadcrumb: {
label: 'notification.inbox',
icon: 'inbox'
menuId: MenuId.notification_inbox
}
},
resolve: {
@ -69,8 +68,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'notification.sent',
breadcrumb: {
label: 'notification.sent',
icon: 'outbox'
menuId: MenuId.notification_sent
}
},
resolve: {
@ -84,8 +82,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'notification.templates',
breadcrumb: {
label: 'notification.templates',
icon: 'mdi:message-draw'
menuId: MenuId.notification_templates
}
},
resolve: {
@ -99,8 +96,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'notification.recipients',
breadcrumb: {
label: 'notification.recipients',
icon: 'contacts'
menuId: MenuId.notification_recipients
},
},
resolve: {
@ -114,8 +110,7 @@ const routes: Routes = [
auth: [Authority.TENANT_ADMIN, Authority.SYS_ADMIN],
title: 'notification.rules',
breadcrumb: {
label: 'notification.rules',
icon: 'mdi:message-cog'
menuId: MenuId.notification_rules
}
},
resolve: {

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

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

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

Loading…
Cancel
Save