Browse Source

Merge remote-tracking branch 'upstream/master' into feature/dashboard-or-conditions

# Conflicts:
#	ui-ngx/src/app/modules/home/components/filter/filter-dialog.component.html
#	ui-ngx/src/app/modules/home/components/filter/key-filter-list.component.html
#	ui-ngx/src/app/modules/home/components/filter/user-filter-dialog.component.html
pull/15394/head
Vladyslav_Prykhodko 6 months ago
parent
commit
238ce55c98
  1. 2
      common/edge-api/src/main/proto/edge.proto
  2. 206
      ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html
  3. 40
      ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html
  4. 8
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html
  5. 58
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html
  6. 60
      ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html
  7. 26
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html
  8. 56
      ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html
  9. 32
      ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html
  10. 18
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html
  11. 26
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html
  12. 24
      ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html
  13. 60
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html
  14. 22
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html
  15. 13
      ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html
  16. 9
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html
  17. 206
      ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html
  18. 89
      ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html
  19. 81
      ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html
  20. 32
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html
  21. 20
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html
  22. 26
      ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html
  23. 22
      ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html
  24. 90
      ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html
  25. 8
      ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html
  26. 8
      ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html
  27. 9
      ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html
  28. 14
      ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html
  29. 220
      ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html
  30. 46
      ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html
  31. 180
      ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html
  32. 12
      ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html
  33. 168
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html
  34. 60
      ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html
  35. 64
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  36. 21
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html
  37. 764
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  38. 430
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  39. 28
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-state.component.html
  40. 100
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html
  41. 81
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.html
  42. 24
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/add-new-breakpoint-dialog.component.html
  43. 50
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/dashboard-layout.component.html
  44. 387
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/manage-dashboard-layouts-dialog.component.html
  45. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.html
  46. 12
      ui-ngx/src/app/modules/home/components/dashboard-page/layout/select-dashboard-breakpoint.component.html
  47. 30
      ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html
  48. 10
      ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html
  49. 28
      ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html
  50. 10
      ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html
  51. 88
      ui-ngx/src/app/modules/home/components/dashboard/dashboard.component.html
  52. 8
      ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html
  53. 14
      ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html
  54. 70
      ui-ngx/src/app/modules/home/components/details-panel.component.html
  55. 50
      ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html
  56. 9
      ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html
  57. 18
      ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html
  58. 403
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.html
  59. 22
      ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts
  60. 10
      ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html
  61. 40
      ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html
  62. 7
      ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html
  63. 613
      ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html
  64. 44
      ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html
  65. 8
      ui-ngx/src/app/modules/home/components/event/event-table-header.component.html
  66. 8
      ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html
  67. 21
      ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html
  68. 70
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html
  69. 92
      ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html
  70. 86
      ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html
  71. 25
      ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html
  72. 94
      ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html
  73. 30
      ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html
  74. 26
      ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html
  75. 143
      ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html
  76. 8
      ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html
  77. 8
      ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html
  78. 20
      ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html
  79. 75
      ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html
  80. 8
      ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts
  81. 56
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html
  82. 8
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.html
  83. 83
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html
  84. 6
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html
  85. 58
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html
  86. 19
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html
  87. 175
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html
  88. 2
      ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts
  89. 97
      ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html
  90. 131
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html
  91. 43
      ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html
  92. 20
      ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html
  93. 107
      ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html
  94. 9
      ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html
  95. 111
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  96. 9
      ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html
  97. 32
      ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html
  98. 14
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html
  99. 56
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html
  100. 19
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html

2
common/edge-api/src/main/proto/edge.proto

@ -48,9 +48,11 @@ enum EdgeVersion {
V_4_2_1_2 = 14; V_4_2_1_2 = 14;
V_4_2_2 = 4220; V_4_2_2 = 4220;
V_4_2_2_1 = 4221; V_4_2_2_1 = 4221;
V_4_2_2_2 = 4222;
V_4_3_0_1 = 15; V_4_3_0_1 = 15;
V_4_3_1 = 4310; V_4_3_1 = 4310;
V_4_3_1_1 = 4311; V_4_3_1_1 = 4311;
V_4_3_1_2 = 4312;
V_4_4_0 = 4400; V_4_4_0 = 4400;
V_LATEST = 99999; V_LATEST = 99999;

206
ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html

@ -25,9 +25,12 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
<div *ngIf="!(isLoading$ | async)"></div> </mat-progress-bar>
} @else {
<div></div>
}
<div mat-dialog-content> <div mat-dialog-content>
<form [formGroup]="aiModelForms"> <form [formGroup]="aiModelForms">
<section class="tb-form-panel outlined no-border no-padding"> <section class="tb-form-panel outlined no-border no-padding">
@ -35,12 +38,16 @@
<mat-label translate>ai-models.name</mat-label> <mat-label translate>ai-models.name</mat-label>
<input matInput required formControlName="name"> <input matInput required formControlName="name">
<mat-hint></mat-hint> <mat-hint></mat-hint>
<mat-error *ngIf="aiModelForms.get('name').hasError('required') || aiModelForms.get('name').hasError('pattern')"> @if (aiModelForms.get('name').hasError('required') || aiModelForms.get('name').hasError('pattern')) {
{{ 'ai-models.name-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.name-required' | translate }}
<mat-error *ngIf="aiModelForms.get('name').hasError('maxlength')"> </mat-error>
{{ 'ai-models.name-max-length' | translate }} }
</mat-error> @if (aiModelForms.get('name').hasError('maxlength')) {
<mat-error>
{{ 'ai-models.name-max-length' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
</section> </section>
<div formGroupName="configuration" class="tb-form-panel no-border no-padding"> <div formGroupName="configuration" class="tb-form-panel no-border no-padding">
@ -50,9 +57,11 @@
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.ai-provider</mat-label> <mat-label translate>ai-models.ai-provider</mat-label>
<mat-select formControlName="provider"> <mat-select formControlName="provider">
<mat-option *ngFor="let provider of providerMap" [value]="provider"> @for (provider of providerMap; track provider) {
{{providerTranslationMap.get(provider) | translate}} <mat-option [value]="provider">
</mat-option> {{providerTranslationMap.get(provider) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div formGroupName="providerConfig" class="tb-form-panel no-border no-padding no-gap"> <div formGroupName="providerConfig" class="tb-form-panel no-border no-padding no-gap">
@ -61,30 +70,36 @@
<mat-label translate>ai-models.personal-access-token</mat-label> <mat-label translate>ai-models.personal-access-token</mat-label>
<input type="password" required matInput formControlName="personalAccessToken" autocomplete="new-password"> <input type="password" required matInput formControlName="personalAccessToken" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('required') ||
aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.personalAccessToken').hasError('pattern')) {
{{ 'ai-models.personal-access-token-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.personal-access-token-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('projectId')) { @if (providerFieldsList.includes('projectId')) {
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.project-id</mat-label> <mat-label translate>ai-models.project-id</mat-label>
<input matInput required formControlName="projectId"> <input matInput required formControlName="projectId">
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.projectId').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.projectId').hasError('required') ||
aiModelForms.get('configuration.providerConfig.projectId').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.projectId').hasError('pattern')) {
{{ 'ai-models.project-id-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.project-id-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('location')) { @if (providerFieldsList.includes('location')) {
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.location</mat-label> <mat-label translate>ai-models.location</mat-label>
<input matInput required formControlName="location"> <input matInput required formControlName="location">
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.location').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.location').hasError('required') ||
aiModelForms.get('configuration.providerConfig.location').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.location').hasError('pattern')) {
{{ 'ai-models.location-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.location-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('serviceAccountKey')) { @if (providerFieldsList.includes('serviceAccountKey')) {
@ -104,10 +119,12 @@
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.endpoint</mat-label> <mat-label translate>ai-models.endpoint</mat-label>
<input required matInput formControlName="endpoint"> <input required matInput formControlName="endpoint">
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.endpoint').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.endpoint').hasError('required') ||
aiModelForms.get('configuration.providerConfig.endpoint').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.endpoint').hasError('pattern')) {
{{ 'ai-models.endpoint-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.endpoint-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('serviceVersion')) { @if (providerFieldsList.includes('serviceVersion')) {
@ -120,10 +137,12 @@
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.baseurl</mat-label> <mat-label translate>ai-models.baseurl</mat-label>
<input required matInput formControlName="baseUrl"> <input required matInput formControlName="baseUrl">
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.baseUrl').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.baseUrl').hasError('required') ||
aiModelForms.get('configuration.providerConfig.baseUrl').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.baseUrl').hasError('pattern')) {
{{ 'ai-models.baseurl-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.baseurl-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('apiKey')) { @if (providerFieldsList.includes('apiKey')) {
@ -131,30 +150,36 @@
<mat-label translate>ai-models.api-key</mat-label> <mat-label translate>ai-models.api-key</mat-label>
<input type="password" matInput formControlName="apiKey" [required]="apiKeyRequired" autocomplete="new-password"> <input type="password" matInput formControlName="apiKey" [required]="apiKeyRequired" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.apiKey').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.apiKey').hasError('required') ||
aiModelForms.get('configuration.providerConfig.apiKey').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.apiKey').hasError('pattern')) {
{{ ( provider === aiProvider.OPENAI ? 'ai-models.api-key-open-ai-required' : 'ai-models.api-key-required') | translate }} <mat-error>
</mat-error> {{ ( provider === aiProvider.OPENAI ? 'ai-models.api-key-open-ai-required' : 'ai-models.api-key-required') | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('region')) { @if (providerFieldsList.includes('region')) {
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.region</mat-label> <mat-label translate>ai-models.region</mat-label>
<input required matInput formControlName="region"> <input required matInput formControlName="region">
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.region').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.region').hasError('required') ||
aiModelForms.get('configuration.providerConfig.region').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.region').hasError('pattern')) {
{{ 'ai-models.region-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.region-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('accessKeyId')) { @if (providerFieldsList.includes('accessKeyId')) {
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.access-key-id</mat-label> <mat-label translate>ai-models.access-key-id</mat-label>
<input required matInput formControlName="accessKeyId"> <input required matInput formControlName="accessKeyId">
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('required') ||
aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.accessKeyId').hasError('pattern')) {
{{ 'ai-models.access-key-id-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.access-key-id-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (providerFieldsList.includes('secretAccessKey')) { @if (providerFieldsList.includes('secretAccessKey')) {
@ -162,10 +187,12 @@
<mat-label translate>ai-models.secret-access-key</mat-label> <mat-label translate>ai-models.secret-access-key</mat-label>
<input type="password" required matInput formControlName="secretAccessKey" autocomplete="new-password"> <input type="password" required matInput formControlName="secretAccessKey" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('required') ||
aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.secretAccessKey').hasError('pattern')) {
{{ 'ai-models.secret-access-key-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.secret-access-key-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (provider === aiProvider.OLLAMA) { @if (provider === aiProvider.OLLAMA) {
@ -185,19 +212,23 @@
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.username</mat-label> <mat-label translate>ai-models.username</mat-label>
<input required matInput formControlName="username"> <input required matInput formControlName="username">
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.auth.username').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.auth.username').hasError('required') ||
aiModelForms.get('configuration.providerConfig.auth.username').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.auth.username').hasError('pattern')) {
{{ 'ai-models.username-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.username-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block flex-1" appearance="outline"> <mat-form-field class="mat-block flex-1" appearance="outline">
<mat-label translate>ai-models.password</mat-label> <mat-label translate>ai-models.password</mat-label>
<input type="password" required matInput formControlName="password" autocomplete="new-password"> <input type="password" required matInput formControlName="password" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.auth.password').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.auth.password').hasError('required') ||
aiModelForms.get('configuration.providerConfig.auth.password').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.auth.password').hasError('pattern')) {
{{ 'ai-models.password-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.password-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
@if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.TOKEN) { @if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.TOKEN) {
@ -205,10 +236,12 @@
<mat-label translate>ai-models.token</mat-label> <mat-label translate>ai-models.token</mat-label>
<input type="password" required matInput formControlName="token" autocomplete="new-password"> <input type="password" required matInput formControlName="token" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="aiModelForms.get('configuration.providerConfig.auth.token').hasError('required') || @if (aiModelForms.get('configuration.providerConfig.auth.token').hasError('required') ||
aiModelForms.get('configuration.providerConfig.auth.token').hasError('pattern')"> aiModelForms.get('configuration.providerConfig.auth.token').hasError('pattern')) {
{{ 'ai-models.token-required' | translate }} <mat-error>
</mat-error> {{ 'ai-models.token-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
} }
</div> </div>
@ -240,14 +273,15 @@
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="temperature" <input matInput formControlName="temperature"
type="number" min="0" step="0.1" placeholder="{{ 'ai-models.set' | translate }}"> type="number" min="0" step="0.1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix @if (aiModelForms.get('configuration').get('temperature').hasError('min')) {
matTooltipPosition="above" <mat-icon matSuffix
matTooltipClass="tb-error-tooltip" matTooltipPosition="above"
[matTooltip]="'ai-models.temperature-min' | translate" matTooltipClass="tb-error-tooltip"
*ngIf="aiModelForms.get('configuration').get('temperature').hasError('min')" [matTooltip]="'ai-models.temperature-min' | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
}
</mat-form-field> </mat-form-field>
</div> </div>
} }
@ -259,15 +293,16 @@
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="topP" <input matInput formControlName="topP"
type="number" min="0" step="0.1" max="1" placeholder="{{ 'ai-models.set' | translate }}"> type="number" min="0" step="0.1" max="1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix @if (aiModelForms.get('configuration').get('topP').hasError('min') ||
matTooltipPosition="above" aiModelForms.get('configuration').get('topP').hasError('max')) {
matTooltipClass="tb-error-tooltip" <mat-icon matSuffix
[matTooltip]="'ai-models.top-p-min-max' | translate" matTooltipPosition="above"
*ngIf="aiModelForms.get('configuration').get('topP').hasError('min') || matTooltipClass="tb-error-tooltip"
aiModelForms.get('configuration').get('topP').hasError('max')" [matTooltip]="'ai-models.top-p-min-max' | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
}
</mat-form-field> </mat-form-field>
</div> </div>
} }
@ -279,14 +314,15 @@
<mat-form-field appearance="outline" class="number" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="number" subscriptSizing="dynamic">
<input matInput formControlName="topK" <input matInput formControlName="topK"
type="number" min="0" step="1" placeholder="{{ 'ai-models.set' | translate }}"> type="number" min="0" step="1" placeholder="{{ 'ai-models.set' | translate }}">
<mat-icon matSuffix @if (aiModelForms.get('configuration').get('topK').hasError('min')) {
matTooltipPosition="above" <mat-icon matSuffix
matTooltipClass="tb-error-tooltip" matTooltipPosition="above"
[matTooltip]="'ai-models.top-k-min' | translate" matTooltipClass="tb-error-tooltip"
*ngIf="aiModelForms.get('configuration').get('topK').hasError('min')" [matTooltip]="'ai-models.top-k-min' | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
}
</mat-form-field> </mat-form-field>
</div> </div>
} }

40
ui-ngx/src/app/modules/home/components/ai-model/check-connectivity-dialog.component.html

@ -26,25 +26,31 @@
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content> <div mat-dialog-content>
<div class="flex h-full flex-1 flex-col items-center justify-center"> <div class="flex h-full flex-1 flex-col items-center justify-center">
<mat-progress-spinner color="warn" mode="indeterminate" @if (!showCheckSuccess && !checkErrMsg) {
strokeWidth="5" *ngIf="!showCheckSuccess && !checkErrMsg"> <mat-progress-spinner color="warn" mode="indeterminate"
</mat-progress-spinner> strokeWidth="5">
<div *ngIf="showCheckSuccess"> </mat-progress-spinner>
<div class="flex flex-1 items-center justify-center"> }
<mat-icon class="success connection-icon">check_circle</mat-icon> @if (showCheckSuccess) {
<div>
<div class="flex flex-1 items-center justify-center">
<mat-icon class="success connection-icon">check_circle</mat-icon>
</div>
<span class="connection-status success">{{ "ai-models.check-connectivity-success" | translate }}</span>
</div> </div>
<span class="connection-status success">{{ "ai-models.check-connectivity-success" | translate }}</span> }
</div> @if (checkErrMsg) {
<div *ngIf="checkErrMsg" class="w-full"> <div class="w-full">
<div class="flex flex-1 items-center justify-center"> <div class="flex flex-1 items-center justify-center">
<mat-icon class="error connection-icon">cancel</mat-icon> <mat-icon class="error connection-icon">cancel</mat-icon>
</div>
<div class="connection-status error">{{ "ai-models.check-connectivity-failed" | translate }}</div>
<tb-json-object-edit class="json-editor"
readonly
[ngModel]="checkErrMsg">
</tb-json-object-edit>
</div> </div>
<div class="connection-status error">{{ "ai-models.check-connectivity-failed" | translate }}</div> }
<tb-json-object-edit class="json-editor"
readonly
[ngModel]="checkErrMsg">
</tb-json-object-edit>
</div>
</div> </div>
</div> </div>
<div mat-dialog-actions class="justify-end"> <div mat-dialog-actions class="justify-end">

8
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html

@ -102,9 +102,11 @@
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</div> </div>
<div *ngIf="!configFormGroup.get('clearRule').value"> @if (!configFormGroup.get('clearRule').value) {
<span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span> <div>
</div> <span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span>
</div>
}
<div [class.!hidden]="configFormGroup.get('clearRule').value"> <div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary" <button mat-stroked-button color="primary"
[disabled]="disabledClearRuleButton" [disabled]="disabledClearRuleButton"

58
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html

@ -15,11 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<ng-container *ngIf="panelMode; else componentMode"> @if (panelMode) {
<ng-container *ngTemplateOutlet="alarmRuleFilterPanel"></ng-container> <ng-container *ngTemplateOutlet="alarmRuleFilterPanel"></ng-container>
</ng-container> } @else {
<ng-template #componentMode> @if (buttonMode) {
<ng-container *ngIf="buttonMode; else alarmFilter">
<button color="primary" <button color="primary"
matTooltip="{{buttonDisplayValue}}" matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above" matTooltipPosition="above"
@ -27,8 +26,47 @@
(click)="toggleAlarmRuleFilterPanel($event)"> (click)="toggleAlarmRuleFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}} <mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button> </button>
</ng-container> } @else {
</ng-template> <div class="tb-form-panel tb-alarm-filter-config-component no-padding no-border" [formGroup]="alarmRuleFilterConfigForm">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm-rule.alarm-rule-type-list</div>
<tb-entity-subtype-list subscriptSizing="dynamic"
formControlName="name"
appearance="outline"
[additionalClasses]="['tb-chips', 'flex']"
[entityType]="entityType.CALCULATED_FIELD">
</tb-entity-subtype-list>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>entity.entity-type</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
@for (type of listEntityTypes; track type) {
<mat-option [value]="type">
{{ entityTypeTranslations.get(type)?.type | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@if (alarmRuleFilterConfigForm.get('entityType').value) {
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>entity.entities</div>
<tb-entity-list appearance="outline"
subscriptSizing="dynamic"
class="flex flex-1"
inlineField
syncIdsWithDB
labelText="{{'entity.entity-list' | translate}}"
[entityType]="alarmRuleFilterConfigForm.get('entityType').value"
formControlName="entities">
</tb-entity-list>
</div>
}
</div>
}
}
<ng-template #alarmRuleFilterPanel> <ng-template #alarmRuleFilterPanel>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()"> <form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="alarmFilter"></ng-container> <ng-container *ngTemplateOutlet="alarmFilter"></ng-container>
@ -70,9 +108,11 @@
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}"> <mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option> <mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
<mat-option *ngFor="let type of listEntityTypes" [value]="type"> @for (type of listEntityTypes; track type) {
{{ entityTypeTranslations.get(type)?.type | translate }} <mat-option [value]="type">
</mat-option> {{ entityTypeTranslations.get(type)?.type | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

60
ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules.component.html

@ -15,26 +15,28 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-details-buttons xs:flex xs:flex-col" *ngIf="!standalone"> @if (!standalone) {
<button mat-raised-button color="primary" <div class="tb-details-buttons xs:flex xs:flex-col">
[disabled]="(isLoading$ | async)" <button mat-raised-button color="primary"
(click)="onEntityAction($event, 'open')" [disabled]="(isLoading$ | async)"
[class.!hidden]="isEdit || isDetailsPage"> (click)="onEntityAction($event, 'open')"
{{ 'common.open-details-page' | translate }} [class.!hidden]="isEdit || isDetailsPage">
</button> {{ 'common.open-details-page' | translate }}
<button mat-raised-button color="primary" </button>
[disabled]="(isLoading$ | async)" <button mat-raised-button color="primary"
(click)="onEntityAction($event, 'export')" [disabled]="(isLoading$ | async)"
[class.!hidden]="isEdit"> (click)="onEntityAction($event, 'export')"
{{ 'action.export' | translate }} [class.!hidden]="isEdit">
</button> {{ 'action.export' | translate }}
<button mat-raised-button color="primary" </button>
[disabled]="(isLoading$ | async)" <button mat-raised-button color="primary"
(click)="onEntityAction($event, 'delete')" [disabled]="(isLoading$ | async)"
[class.!hidden]="hideDelete() || isEdit"> (click)="onEntityAction($event, 'delete')"
{{ 'action.delete' | translate }} [class.!hidden]="hideDelete() || isEdit">
</button> {{ 'action.delete' | translate }}
</div> </button>
</div>
}
<div class="mat-padding flex flex-1 flex-col" [formGroup]="entityForm"> <div class="mat-padding flex flex-1 flex-col" [formGroup]="entityForm">
<div class="tb-form-panel no-border no-padding"> <div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel no-gap"> <div class="tb-form-panel no-gap">
@ -55,11 +57,11 @@
<mat-hint></mat-hint> <mat-hint></mat-hint>
</mat-form-field> </mat-form-field>
<tb-entity-debug-settings-button <tb-entity-debug-settings-button
formControlName="debugSettings" formControlName="debugSettings"
style="margin-bottom: 22px" style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD" [entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig" [additionalActionConfig]="additionalDebugActionConfig"
/> />
</div> </div>
<tb-entity-select formControlName="entityId" <tb-entity-select formControlName="entityId"
appearance="outline" appearance="outline"
@ -107,9 +109,11 @@
</button> </button>
} }
</div> </div>
<div *ngIf="!configFormGroup.get('clearRule').value"> @if (!configFormGroup.get('clearRule').value) {
<span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span> <div>
</div> <span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span>
</div>
}
<div [class.!hidden]="configFormGroup.get('clearRule').value"> <div [class.!hidden]="configFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary" <button mat-stroked-button color="primary"
[disabled]="!isEditValue" [disabled]="!isEditValue"

26
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-rule-condition-dialog.component.html

@ -119,16 +119,18 @@
</mat-expansion-panel> </mat-expansion-panel>
</div> </div>
} }
</div>
</div> </div>
</div>
<section class="tb-form-panel stroked"> <section class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.type' | translate }}</div> <div class="tb-form-panel-title">{{ 'alarm-rule.type' | translate }}</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs"> <tb-toggle-select formControlName="type" selectMediaBreakpoint="xs">
<tb-toggle-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"> @for (alarmConditionType of alarmConditionTypes; track alarmConditionType) {
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} <tb-toggle-option [value]="alarmConditionType">
</tb-toggle-option> {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</tb-toggle-option>
}
</tb-toggle-select> </tb-toggle-select>
</div> </div>
@if (isNoData) { @if (isNoData) {
@ -157,13 +159,17 @@
<mat-form-field class="mat-block" appearance="outline"> <mat-form-field class="mat-block" appearance="outline">
<mat-select formControlName="unit" required <mat-select formControlName="unit" required
placeholder="{{ 'alarm-rule.condition-duration-time-unit' | translate }}"> placeholder="{{ 'alarm-rule.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> @for (timeUnit of timeUnits; track timeUnit) {
{{ timeUnitTranslations.get(timeUnit) | translate }} <mat-option [value]="timeUnit">
</mat-option> {{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
}
</mat-select> </mat-select>
<mat-error *ngIf="conditionFormGroup.get('unit').hasError('required')"> @if (conditionFormGroup.get('unit').hasError('required')) {
{{ 'alarm-rule.condition-duration-time-unit-required' | translate }} <mat-error>
</mat-error> {{ 'alarm-rule.condition-duration-time-unit-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>

56
ui-ngx/src/app/modules/home/components/alarm-rules/cf-alarm-schedule.component.html

@ -41,10 +41,12 @@
@if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME) { @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.SPECIFIC_TIME) {
<section> <section>
<mat-chip-listbox multiple formControlName="daysOfWeek"> <mat-chip-listbox multiple formControlName="daysOfWeek">
<mat-chip-option *ngFor="let day of allDays; let i = index;" @for (day of allDays; track day; let i = $index) {
[value]="i+1"> <mat-chip-option
{{ dayOfWeekTranslationsArray[day] | translate }} [value]="i+1">
</mat-chip-option> {{ dayOfWeekTranslationsArray[day] | translate }}
</mat-chip-option>
}
</mat-chip-listbox> </mat-chip-listbox>
<tb-error class="mb-2 block" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required') <tb-error class="mb-2 block" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required')
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error> ? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
@ -71,30 +73,32 @@
} }
@if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM) { @if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM) {
<section class="flex flex-col gap-4"> <section class="flex flex-col gap-4">
<div *ngFor="let day of allDays" class="flex flex-col gap-4" formArrayName="items"> @for (day of allDays; track day) {
<div class="flex flex-row items-center justify-start gap-2 xs:flex-col xs:items-start xs:justify-center" [formGroupName]="''+day"> <div class="flex flex-col gap-4" formArrayName="items">
<mat-chip-listbox formControlName="enabled"> <div class="flex flex-row items-center justify-start gap-2 xs:flex-col xs:items-start xs:justify-center" [formGroupName]="''+day">
<mat-chip-option class="w-32" color="primary" (selectionChange)="changeCustomScheduler($event, day)" [value]="true">{{ dayOfWeekTranslationsArray[day] | translate }}</mat-chip-option> <mat-chip-listbox formControlName="enabled">
</mat-chip-listbox> <mat-chip-option class="w-32" color="primary" (selectionChange)="changeCustomScheduler($event, day)" [value]="true">{{ dayOfWeekTranslationsArray[day] | translate }}</mat-chip-option>
<div class="flex flex-1 flex-row gap-2"> </mat-chip-listbox>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <div class="flex flex-1 flex-row gap-2">
<mat-label translate>alarm-rule.schedule-time-from</mat-label> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> <mat-label translate>alarm-rule.schedule-time-from</mat-label>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker"> <mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
</mat-form-field> <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> </mat-form-field>
<mat-label translate>alarm-rule.schedule-time-to</mat-label> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> <mat-label translate>alarm-rule.schedule-time-to</mat-label>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> <mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
</mat-form-field> <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</div> </mat-form-field>
<div class="flex flex-1 items-center justify-center text-center sm:max-w-[120px]" </div>
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))"> <div class="flex flex-1 items-center justify-center text-center sm:max-w-[120px]"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div>
</div> </div>
</div> </div>
</div> }
<tb-error class="block" [error]="alarmScheduleForm.get('staticValue.items').hasError('dayOfWeeks') <tb-error class="block" [error]="alarmScheduleForm.get('staticValue.items').hasError('dayOfWeeks')
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error> ? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
</section> </section>

32
ui-ngx/src/app/modules/home/components/alarm-rules/create-cf-alarm-rules.component.html

@ -26,30 +26,34 @@
<mat-select formControlName="severity" <mat-select formControlName="severity"
required required
placeholder="{{ 'alarm-rule.select-alarm-severity' | translate }}"> placeholder="{{ 'alarm-rule.select-alarm-severity' | translate }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity" @for (alarmSeverity of alarmSeverities; track alarmSeverity) {
[disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], index)"> <mat-option [value]="alarmSeverity"
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} [disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], index)">
</mat-option> {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<tb-cf-alarm-rule formControlName="alarmRule" [arguments]="arguments" [testScript]="testScript" required class="flex-1"> <tb-cf-alarm-rule formControlName="alarmRule" [arguments]="arguments" [testScript]="testScript" required class="flex-1">
</tb-cf-alarm-rule> </tb-cf-alarm-rule>
</div> </div>
<button *ngIf="!disabled" @if (!disabled) {
class="button-icon" <button
mat-icon-button class="button-icon"
type="button" mat-icon-button
(click)="removeCreateAlarmRule(index)" type="button"
matTooltip="{{ 'action.remove' | translate }}" (click)="removeCreateAlarmRule(index)"
matTooltipPosition="above"> matTooltip="{{ 'action.remove' | translate }}"
<mat-icon>delete</mat-icon> matTooltipPosition="above">
</button> <mat-icon>delete</mat-icon>
</button>
}
</div> </div>
} }
@if (!createAlarmRulesFormArray().controls.length) { @if (!createAlarmRulesFormArray().controls.length) {
<span translate class="tb-prompt required flex items-center justify-center text-base" [class.required]="!disabled"> <span translate class="tb-prompt required flex items-center justify-center text-base" [class.required]="!disabled">
alarm-rule.add-create-alarm-rule-prompt alarm-rule.add-create-alarm-rule-prompt
</span> </span>
} }
<div> <div>

18
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-dialog.component.html

@ -53,14 +53,18 @@
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.icon }}"></mat-icon> <mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.name | translate }}</span> <span>{{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.name | translate }}</span>
</mat-select-trigger> </mat-select-trigger>
<mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType"> @for (valueType of entityKeyValueTypesKeys; track valueType) {
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon> <mat-option [value]="valueType">
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span> <mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
</mat-option> <span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
</mat-option>
}
</mat-select> </mat-select>
<mat-error *ngIf="filterFormGroup.get('valueType').hasError('required')"> @if (filterFormGroup.get('valueType').hasError('required')) {
{{ 'filter.value-type-required' | translate }} <mat-error>
</mat-error> {{ 'filter.value-type-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
</section> </section>

26
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate-no-data-value.component.html

@ -28,15 +28,15 @@
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<input required type="number" matInput formControlName="staticValue" min="1" <input required type="number" matInput formControlName="staticValue" min="1"
placeholder="{{'filter.value' | translate}}"> placeholder="{{'filter.value' | translate}}">
@if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) { @if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) {
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"
[matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.staticValue').hasError('required') ? 'alarm-rule.value-required' : 'alarm-rule.min-value') | translate" [matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.staticValue').hasError('required') ? 'alarm-rule.value-required' : 'alarm-rule.min-value') | translate"
class="tb-error"> class="tb-error">
warning warning
</mat-icon> </mat-icon>
} }
</mat-form-field> </mat-form-field>
} @else { } @else {
<mat-form-field class="w-full flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="w-full flex-1" appearance="outline" subscriptSizing="dynamic">
@ -68,9 +68,11 @@
</ng-container> </ng-container>
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="unit" required placeholder="{{'alarm-rule.time-unit' | translate}}"> <mat-select formControlName="unit" required placeholder="{{'alarm-rule.time-unit' | translate}}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> @for (timeUnit of timeUnits; track timeUnit) {
{{ timeUnitsTranslationMap.get(timeUnit) | translate }} <mat-option [value]="timeUnit">
</mat-option> {{ timeUnitsTranslationMap.get(timeUnit) | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

24
ui-ngx/src/app/modules/home/components/alarm-rules/filter/alarm-rule-filter-predicate.component.html

@ -22,9 +22,11 @@
<div class="tb-form-row no-padding no-border column-xs flex-1/ w-full max-w-30% xs:max-w-full"> <div class="tb-form-row no-padding no-border column-xs flex-1/ w-full max-w-30% xs:max-w-full">
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of stringOperations" [value]="operation"> @for (operation of stringOperations; track operation) {
{{stringOperationTranslationMap.get(stringOperation[operation]) | translate}} <mat-option [value]="operation">
</mat-option> {{stringOperationTranslationMap.get(stringOperation[operation]) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@if (filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA) { @if (filterPredicateFormGroup.get('operation').value !== stringOperation.NO_DATA) {
@ -38,9 +40,11 @@
<div class="tb-form-row no-border no-padding no-border w-full max-w-30%"> <div class="tb-form-row no-border no-padding no-border w-full max-w-30%">
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of numericOperations" [value]="operation"> @for (operation of numericOperations; track operation) {
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} <mat-option [value]="operation">
</mat-option> {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -49,9 +53,11 @@
<div class="tb-form-row no-border no-padding no-border w-full max-w-30%"> <div class="tb-form-row no-border no-padding no-border w-full max-w-30%">
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of booleanOperations" [value]="operation"> @for (operation of booleanOperations; track operation) {
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} <mat-option [value]="operation">
</mat-option> {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

60
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html

@ -26,32 +26,42 @@
#userAutocomplete="matAutocomplete" #userAutocomplete="matAutocomplete"
[displayWith]="displayUserFn" [displayWith]="displayUserFn"
(optionSelected)="selected($event)"> (optionSelected)="selected($event)">
<mat-option *ngIf="displayAssigneeNotSet" [value]="assigneeOptions.noAssignee"> @if (displayAssigneeNotSet) {
<mat-icon class="unassigned-icon">account_circle</mat-icon> <mat-option [value]="assigneeOptions.noAssignee">
<span>{{ assigneeNotSetText | translate }}</span> <mat-icon class="unassigned-icon">account_circle</mat-icon>
</mat-option> <span>{{ assigneeNotSetText | translate }}</span>
<mat-option *ngIf="displayAssignedToCurrentUser" [value]="assigneeOptions.currentUser"> </mat-option>
<mat-icon class="unassigned-icon">account_circle</mat-icon> }
<span>{{ assignedToCurrentUserText | translate }}</span> @if (displayAssignedToCurrentUser) {
</mat-option> <mat-option [value]="assigneeOptions.currentUser">
<mat-option *ngFor="let user of filteredUsers | async" [value]="user" <mat-icon class="unassigned-icon">account_circle</mat-icon>
[class.!hidden]="assigneeId === user.id.id"> <span>{{ assignedToCurrentUserText | translate }}</span>
<span class="user-avatar" [innerHTML]="getUserInitials(user)" </mat-option>
[style.background-color]="getAvatarBgColor(user)"> }
</span> @for (user of filteredUsers | async; track user) {
<div class="user-display-name flex flex-col gap-0.5"> <mat-option [value]="user"
<span *ngIf="user.firstName || user.lastName" [class.!hidden]="assigneeId === user.id.id">
[innerHTML]="getFullName(user) | highlight:searchText"></span> <span class="user-avatar" [innerHTML]="getUserInitials(user)"
<span [innerHTML]="user.email | highlight:searchText"></span> [style.background-color]="getAvatarBgColor(user)">
</div>
</mat-option>
<mat-option *ngIf="!(filteredUsers | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<span style="white-space: normal">
{{ translate.get('user.no-users-matching', {entity: searchText}) | async }}
</span> </span>
</div> <div class="user-display-name flex flex-col gap-0.5">
</mat-option> @if (user.firstName || user.lastName) {
<span
[innerHTML]="getFullName(user) | highlight:searchText"></span>
}
<span [innerHTML]="user.email | highlight:searchText"></span>
</div>
</mat-option>
}
@if (!(filteredUsers | async)?.length) {
<mat-option [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<span style="white-space: normal">
{{ translate.get('user.no-users-matching', {entity: searchText}) | async }}
</span>
</div>
</mat-option>
}
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>

22
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-select.component.html

@ -19,13 +19,21 @@
<mat-form-field [style]="inline ? {width: '100%'} : {marginBottom: '25px'}" <mat-form-field [style]="inline ? {width: '100%'} : {marginBottom: '25px'}"
(click)="openAlarmAssigneeSelectPanel($event)" (click)="openAlarmAssigneeSelectPanel($event)"
subscriptSizing="dynamic" [appearance]="inline ? 'outline' : 'fill'"> subscriptSizing="dynamic" [appearance]="inline ? 'outline' : 'fill'">
<mat-label *ngIf="!inline" translate>alarm.assignee</mat-label> @if (!inline) {
<mat-label translate>alarm.assignee</mat-label>
}
<input matInput readonly formControlName="assignee"> <input matInput readonly formControlName="assignee">
<span *ngIf="assignee" matPrefix class="user-avatar" [class.inline-icon]="inline" @if (assignee) {
[style.background-color]="getAvatarBgColor()"> <span matPrefix class="user-avatar" [class.inline-icon]="inline"
{{ getUserInitials() }} [style.background-color]="getAvatarBgColor()">
</span> {{ getUserInitials() }}
<mat-icon *ngIf="!assignee" matPrefix class="unassigned-icon" [class.inline-icon]="inline">account_circle</mat-icon> </span>
<mat-icon *ngIf="!disabled" matSuffix>arrow_drop_down</mat-icon> }
@if (!assignee) {
<mat-icon matPrefix class="unassigned-icon" [class.inline-icon]="inline">account_circle</mat-icon>
}
@if (!disabled) {
<mat-icon matSuffix>arrow_drop_down</mat-icon>
}
</mat-form-field> </mat-form-field>
</ng-container> </ng-container>

13
ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html

@ -20,12 +20,13 @@
subscriptSizing="dynamic"> subscriptSizing="dynamic">
<mat-label translate>alarm.assignee</mat-label> <mat-label translate>alarm.assignee</mat-label>
<input matInput readonly [value]="getAssignee()"> <input matInput readonly [value]="getAssignee()">
<span *ngIf="userAssigned; else unassigned" matPrefix class="user-avatar" @if (userAssigned) {
[style.background-color]="getAvatarBgColor(alarm.assignee)"> <span matPrefix class="user-avatar"
{{ getUserInitials(alarm.assignee) }} [style.background-color]="getAvatarBgColor(alarm.assignee)">
</span> {{ getUserInitials(alarm.assignee) }}
<ng-template #unassigned> </span>
} @else {
<mat-icon matPrefix class="material-icons unassigned-icon">{{ alarm?.assigneeId ? 'no_accounts' : 'account_circle' }}</mat-icon> <mat-icon matPrefix class="material-icons unassigned-icon">{{ alarm?.assigneeId ? 'no_accounts' : 'account_circle' }}</mat-icon>
</ng-template> }
<mat-icon matSuffix>arrow_drop_down</mat-icon> <mat-icon matSuffix>arrow_drop_down</mat-icon>
</mat-form-field> </mat-form-field>

9
ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html

@ -22,9 +22,12 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> </mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content style="padding: 0"> <div mat-dialog-content style="padding: 0">
<tb-alarm-comment [alarmId]="alarmId" <tb-alarm-comment [alarmId]="alarmId"
[alarmActivityOnly]="true"> [alarmActivityOnly]="true">

206
ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html

@ -49,104 +49,110 @@
</div> </div>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col">
<ng-container *ngIf="!isDirectionAscending()"> @if (!isDirectionAscending()) {
<ng-container *ngTemplateOutlet="commentInput"></ng-container> <ng-container *ngTemplateOutlet="commentInput"></ng-container>
</ng-container> }
<div class="comments-container flex flex-col gap-3" style="padding: 24px" <div class="comments-container flex flex-col gap-3" style="padding: 24px"
[class.asc]="isDirectionAscending()" [class.asc]="isDirectionAscending()"
[class.activity-only]="alarmActivityOnly"> [class.activity-only]="alarmActivityOnly">
<div class="flex-1" *ngFor="let displayDataElement of displayData; index as i"> @for (displayDataElement of displayData; track displayDataElement; let i = $index) {
<div class="system-comment" *ngIf="displayDataElement.isSystemComment; else userComment"> <div class="flex-1">
<span class="system-text" style="margin-right: 8px"> @if (displayDataElement.isSystemComment) {
{{ displayDataElement.commentText }} <div class="system-comment">
</span> <span class="system-text" style="margin-right: 8px">
<span class="time" style="padding: 3px" {{ displayDataElement.commentText }}
[matTooltip]="displayDataElement.createdTime" </span>
matTooltipPosition="right"> <span class="time" style="padding: 3px"
{{ displayDataElement.createdDateAgo }} [matTooltip]="displayDataElement.createdTime"
</span> matTooltipPosition="right">
</div> {{ displayDataElement.createdDateAgo }}
<ng-template #userComment> </span>
<div class="user-comment flex flex-row items-center justify-start gap-2"
*ngIf="!displayDataElement.edit; else commentEditing"
(mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)"
(mouseleave)="onCommentMouseLeave(i)">
<div *ngIf="displayDataElement.userExists; else userDeleted"
class="user-avatar flex flex-row items-center justify-center self-start xs:!hidden"
[style.background-color]="displayDataElement.avatarBgColor">
{{ getUserInitials(displayDataElement.displayName) }}
</div>
<ng-template #userDeleted>
<mat-icon class="mat-icon tb-mat-28 xs:!hidden">no_accounts</mat-icon>
</ng-template>
<div class="flex flex-1 flex-col" style="gap: 5px">
<div class="flex flex-row items-center justify-start gap-2">
<span class="user-name">{{ displayDataElement.displayName }}</span>
<span class="time"
[matTooltip]="displayDataElement.createdTime"
matTooltipPosition="right">
{{ displayDataElement.createdDateAgo }}
</span>
<span class="time" *ngIf="displayDataElement.isEdited"
matTooltip="{{ displayDataElement.editedDateAgo }} {{ displayDataElement.editedTime }}"
matTooltipPosition="right">
Edited
</span>
</div>
<span class="text">{{ displayDataElement.commentText }}</span>
</div>
<div class="action-buttons flex flex-row xs:flex-col"
[class.show-buttons]="displayDataElement.showActions">
<button mat-icon-button
type="button"
(click)="editComment(displayDataElement.commentId)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button mat-icon-button
type="button"
(click)="deleteComment(displayDataElement.commentId)">
<mat-icon class="material-icons">delete</mat-icon>
</button>
</div> </div>
</div> } @else {
<ng-template #commentEditing> @if (!displayDataElement.edit) {
<div class="flex flex-row items-center justify-start gap-2"> <div class="user-comment flex flex-row items-center justify-start gap-2"
<div class="user-avatar flex flex-row items-center justify-center" (mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)"
[style.background-color]="displayDataElement.avatarBgColor"> (mouseleave)="onCommentMouseLeave(i)">
{{ getUserInitials(displayDataElement.displayName) }} @if (displayDataElement.userExists) {
</div> <div
<mat-form-field class="mat-block tb-appearance-transparent flex-1"> class="user-avatar flex flex-row items-center justify-center self-start xs:!hidden"
<textarea matInput [style.background-color]="displayDataElement.avatarBgColor">
type="text" {{ getUserInitials(displayDataElement.displayName) }}
placeholder="{{ 'alarm-activity.add' | translate }}" </div>
cdkTextareaAutosize } @else {
cdkAutosizeMinRows="1" <mat-icon class="mat-icon tb-mat-28 xs:!hidden">no_accounts</mat-icon>
cols="1" }
formControlName="alarmCommentEdit" <div class="flex flex-1 flex-col" style="gap: 5px">
(keyup.enter)="saveEditedComment(displayDataElement.commentId)" <div class="flex flex-row items-center justify-start gap-2">
(keydown.enter)="$event.preventDefault()"> <span class="user-name">{{ displayDataElement.displayName }}</span>
</textarea> <span class="time"
<div matSuffix class="flex flex-row"> [matTooltip]="displayDataElement.createdTime"
matTooltipPosition="right">
{{ displayDataElement.createdDateAgo }}
</span>
@if (displayDataElement.isEdited) {
<span class="time"
matTooltip="{{ displayDataElement.editedDateAgo }} {{ displayDataElement.editedTime }}"
matTooltipPosition="right">
Edited
</span>
}
</div>
<span class="text">{{ displayDataElement.commentText }}</span>
</div>
<div class="action-buttons flex flex-row xs:flex-col"
[class.show-buttons]="displayDataElement.showActions">
<button mat-icon-button <button mat-icon-button
(click)="cancelEdit(displayDataElement.commentId)" type="button"
type="button"> (click)="editComment(displayDataElement.commentId)">
<mat-icon class="material-icons red-button">close</mat-icon> <mat-icon class="material-icons">edit</mat-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
type="button" type="button"
(click)="saveEditedComment(displayDataElement.commentId)"> (click)="deleteComment(displayDataElement.commentId)">
<mat-icon class="material-icons green-button">check</mat-icon> <mat-icon class="material-icons">delete</mat-icon>
</button> </button>
</div> </div>
</mat-form-field> </div>
</div> } @else {
</ng-template> <div class="flex flex-row items-center justify-start gap-2">
</ng-template> <div class="user-avatar flex flex-row items-center justify-center"
</div> [style.background-color]="displayDataElement.avatarBgColor">
{{ getUserInitials(displayDataElement.displayName) }}
</div>
<mat-form-field class="mat-block tb-appearance-transparent flex-1">
<textarea matInput
type="text"
placeholder="{{ 'alarm-activity.add' | translate }}"
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cols="1"
formControlName="alarmCommentEdit"
(keyup.enter)="saveEditedComment(displayDataElement.commentId)"
(keydown.enter)="$event.preventDefault()">
</textarea>
<div matSuffix class="flex flex-row">
<button mat-icon-button
(click)="cancelEdit(displayDataElement.commentId)"
type="button">
<mat-icon class="material-icons red-button">close</mat-icon>
</button>
<button mat-icon-button
type="button"
(click)="saveEditedComment(displayDataElement.commentId)">
<mat-icon class="material-icons green-button">check</mat-icon>
</button>
</div>
</mat-form-field>
</div>
}
}
</div>
}
</div> </div>
<ng-container *ngIf="isDirectionAscending()"> @if (isDirectionAscending()) {
<ng-container *ngTemplateOutlet="commentInput"></ng-container> <ng-container *ngTemplateOutlet="commentInput"></ng-container>
</ng-container> }
</div> </div>
<ng-template #commentInput> <ng-template #commentInput>
<div style="background-color: white" class="comment-input" <div style="background-color: white" class="comment-input"
@ -154,11 +160,12 @@
[class.oldest-first]="isDirectionAscending()" [class.oldest-first]="isDirectionAscending()"
[class.activity-only]="alarmActivityOnly"> [class.activity-only]="alarmActivityOnly">
<div class="inner-wrap flex flex-row items-center justify-start gap-2"> <div class="inner-wrap flex flex-row items-center justify-start gap-2">
<div class="user-avatar flex flex-row items-center justify-center" @if (userDisplayName$ | async; as userDisplayName) {
*ngIf="userDisplayName$ | async; let userDisplayName" <div class="user-avatar flex flex-row items-center justify-center"
[style.background-color]="getCurrentUserBgColor(userDisplayName)"> [style.background-color]="getCurrentUserBgColor(userDisplayName)">
{{ getUserInitials(userDisplayName) }} {{ getUserInitials(userDisplayName) }}
</div> </div>
}
<mat-form-field class="mat-block tb-appearance-transparent flex-1"> <mat-form-field class="mat-block tb-appearance-transparent flex-1">
<textarea matInput <textarea matInput
type="text" type="text"
@ -170,15 +177,16 @@
(keyup.enter)="saveComment()" (keyup.enter)="saveComment()"
(keydown.enter)="$event.preventDefault()"> (keydown.enter)="$event.preventDefault()">
</textarea> </textarea>
<button mat-icon-button @if (getAlarmCommentValue()) {
type="button" <button mat-icon-button
matSuffix type="button"
*ngIf="getAlarmCommentValue()" matSuffix
(click)="saveComment()"> (click)="saveComment()">
<mat-icon color="primary"> <mat-icon color="primary">
send send
</mat-icon> </mat-icon>
</button> </button>
}
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>

89
ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html

@ -25,9 +25,13 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div> </mat-progress-bar>
}
@if ((isLoading$ | async) === false) {
<div style="height: 4px;"></div>
}
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" style="margin-bottom: 22px"> <fieldset [disabled]="isLoading$ | async" style="margin-bottom: 22px">
<div class="flex flex-row gap-1.5"> <div class="flex flex-row gap-1.5">
@ -43,14 +47,18 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-row gap-1.5"> <div class="flex flex-row gap-1.5">
<mat-form-field *ngIf="alarmFormGroup.get('startTime').value" class="mat-block flex-1"> @if (alarmFormGroup.get('startTime').value) {
<mat-label translate>alarm.start-time</mat-label> <mat-form-field class="mat-block flex-1">
<input matInput formControlName="startTime" readonly> <mat-label translate>alarm.start-time</mat-label>
</mat-form-field> <input matInput formControlName="startTime" readonly>
<mat-form-field *ngIf="alarmFormGroup.get('duration').value" class="mat-block flex-1"> </mat-form-field>
<mat-label translate>alarm.duration</mat-label> }
<input matInput formControlName="duration" readonly> @if (alarmFormGroup.get('duration').value) {
</mat-form-field> <mat-form-field class="mat-block flex-1">
<mat-label translate>alarm.duration</mat-label>
<input matInput formControlName="duration" readonly>
</mat-form-field>
}
</div> </div>
<div class="flex flex-row gap-1.5"> <div class="flex flex-row gap-1.5">
<mat-form-field class="mat-block flex-1"> <mat-form-field class="mat-block flex-1">
@ -71,12 +79,13 @@
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<tb-json-object-edit @if (displayDetails) {
*ngIf="displayDetails" <tb-json-object-edit
formControlName="alarmDetails" formControlName="alarmDetails"
readonly readonly
label="{{ 'alarm.additional-info' | translate }}"> label="{{ 'alarm.additional-info' | translate }}">
</tb-json-object-edit> </tb-json-object-edit>
}
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>
@ -88,25 +97,31 @@
</div> </div>
<div mat-dialog-actions class="flex flex-row"> <div mat-dialog-actions class="flex flex-row">
<span class="flex-1"></span> <span class="flex-1"></span>
<div class="flex flex-row gap-2" *ngIf="alarm$ | async; let alarm;"> @if (alarm$ | async; as alarm;) {
<button *ngIf="allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK || <div class="flex flex-row gap-2">
alarm.status === alarmStatuses.CLEARED_UNACK)" @if (allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK ||
mat-raised-button alarm.status === alarmStatuses.CLEARED_UNACK)) {
color="primary" <button
type="button" mat-raised-button
(click)="acknowledge()" color="primary"
[disabled]="(isLoading$ | async)"> type="button"
{{ 'alarm.acknowledge' | translate }} (click)="acknowledge()"
</button> [disabled]="(isLoading$ | async)">
<button *ngIf="allowClear && (alarm.status === alarmStatuses.ACTIVE_ACK || {{ 'alarm.acknowledge' | translate }}
alarm.status === alarmStatuses.ACTIVE_UNACK)" </button>
mat-raised-button }
color="primary" @if (allowClear && (alarm.status === alarmStatuses.ACTIVE_ACK ||
type="button" alarm.status === alarmStatuses.ACTIVE_UNACK)) {
(click)="clear()" <button
[disabled]="(isLoading$ | async)"> mat-raised-button
{{ 'alarm.clear' | translate }} color="primary"
</button> type="button"
</div> (click)="clear()"
[disabled]="(isLoading$ | async)">
{{ 'alarm.clear' | translate }}
</button>
}
</div>
}
</div> </div>
</form> </form>

81
ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.html

@ -15,11 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<ng-container *ngIf="panelMode; else componentMode"> @if (panelMode) {
<ng-container *ngTemplateOutlet="alarmFilterPanel"></ng-container> <ng-container *ngTemplateOutlet="alarmFilterPanel"></ng-container>
</ng-container> } @else {
<ng-template #componentMode> @if (buttonMode) {
<ng-container *ngIf="buttonMode; else alarmFilter">
<button color="primary" <button color="primary"
matTooltip="{{buttonDisplayValue}}" matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above" matTooltipPosition="above"
@ -27,8 +26,52 @@
(click)="toggleAlarmFilterPanel($event)"> (click)="toggleAlarmFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}} <mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button> </button>
</ng-container> } @else {
</ng-template> <div class="tb-form-panel tb-alarm-filter-config-component no-padding no-border" [formGroup]="alarmFilterConfigForm">
<div class="tb-form-row space-between column-xs">
<div class="fixed-title-width" translate>alarm.alarm-status-list</div>
<mat-chip-listbox multiple formControlName="statusList">
@for (searchStatus of alarmSearchStatuses; track searchStatus) {
<mat-chip-option [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-chip-option>
}
</mat-chip-listbox>
</div>
<div class="tb-form-row space-between column-xs">
<div class="fixed-title-width" translate>alarm.alarm-severity-list</div>
<mat-chip-listbox multiple formControlName="severityList">
@for (alarmSeverity of alarmSeverities; track alarmSeverity) {
<mat-chip-option [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-chip-option>
}
</mat-chip-listbox>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm.alarm-type-list</div>
<tb-entity-subtype-list subscriptSizing="dynamic"
formControlName="typeList"
appearance="outline"
[additionalClasses]="['tb-chips', 'flex']"
[entityType]="entityType.ALARM">
</tb-entity-subtype-list>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>alarm.assignee</div>
<tb-alarm-assignee-select class="flex-1" inline="true"
[userMode]="userMode"
formControlName="assigneeId">
</tb-alarm-assignee-select>
</div>
@if (propagatedFilter) {
<mat-slide-toggle class="mat-slide" formControlName="searchPropagatedAlarms">
{{ 'alarm.search-propagated-alarms' | translate }}
</mat-slide-toggle>
}
</div>
}
}
<ng-template #alarmFilterPanel> <ng-template #alarmFilterPanel>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()"> <form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="alarmFilter"></ng-container> <ng-container *ngTemplateOutlet="alarmFilter"></ng-container>
@ -59,17 +102,21 @@
<div class="tb-form-row space-between column-xs"> <div class="tb-form-row space-between column-xs">
<div class="fixed-title-width" translate>alarm.alarm-status-list</div> <div class="fixed-title-width" translate>alarm.alarm-status-list</div>
<mat-chip-listbox multiple formControlName="statusList"> <mat-chip-listbox multiple formControlName="statusList">
<mat-chip-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus"> @for (searchStatus of alarmSearchStatuses; track searchStatus) {
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }} <mat-chip-option [value]="searchStatus">
</mat-chip-option> {{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-chip-option>
}
</mat-chip-listbox> </mat-chip-listbox>
</div> </div>
<div class="tb-form-row space-between column-xs"> <div class="tb-form-row space-between column-xs">
<div class="fixed-title-width" translate>alarm.alarm-severity-list</div> <div class="fixed-title-width" translate>alarm.alarm-severity-list</div>
<mat-chip-listbox multiple formControlName="severityList"> <mat-chip-listbox multiple formControlName="severityList">
<mat-chip-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"> @for (alarmSeverity of alarmSeverities; track alarmSeverity) {
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} <mat-chip-option [value]="alarmSeverity">
</mat-chip-option> {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-chip-option>
}
</mat-chip-listbox> </mat-chip-listbox>
</div> </div>
<div class="tb-form-row column-xs"> <div class="tb-form-row column-xs">
@ -85,11 +132,13 @@
<div class="fixed-title-width" translate>alarm.assignee</div> <div class="fixed-title-width" translate>alarm.assignee</div>
<tb-alarm-assignee-select class="flex-1" inline="true" <tb-alarm-assignee-select class="flex-1" inline="true"
[userMode]="userMode" [userMode]="userMode"
formControlName="assigneeId"> formControlName="assigneeId">
</tb-alarm-assignee-select> </tb-alarm-assignee-select>
</div> </div>
<mat-slide-toggle class="mat-slide" *ngIf="propagatedFilter" formControlName="searchPropagatedAlarms"> @if (propagatedFilter) {
{{ 'alarm.search-propagated-alarms' | translate }} <mat-slide-toggle class="mat-slide" formControlName="searchPropagatedAlarms">
</mat-slide-toggle> {{ 'alarm.search-propagated-alarms' | translate }}
</mat-slide-toggle>
}
</div> </div>
</ng-template> </ng-template>

32
ui-ngx/src/app/modules/home/components/alias/aliases-entity-autocomplete.component.html

@ -22,23 +22,29 @@
formControlName="entityInfo" formControlName="entityInfo"
[required]="required" [required]="required"
[matAutocomplete]="entityInfoAutocomplete"> [matAutocomplete]="entityInfoAutocomplete">
<button *ngIf="selectEntityInfoFormGroup.get('entityInfo').value && !disabled" @if (selectEntityInfoFormGroup.get('entityInfo').value && !disabled) {
type="button" <button
matSuffix mat-icon-button aria-label="Clear" type="button"
(click)="clear()"> matSuffix mat-icon-button aria-label="Clear"
<mat-icon class="material-icons">close</mat-icon> (click)="clear()">
</button> <mat-icon class="material-icons">close</mat-icon>
</button>
}
<mat-autocomplete <mat-autocomplete
class="tb-autocomplete" class="tb-autocomplete"
#entityInfoAutocomplete="matAutocomplete" #entityInfoAutocomplete="matAutocomplete"
[displayWith]="displayEntityInfoFn"> [displayWith]="displayEntityInfoFn">
<mat-option *ngFor="let entityInfo of filteredEntityInfos | async" [value]="entityInfo"> @for (entityInfo of filteredEntityInfos | async; track entityInfo) {
<span [innerHTML]="entityInfo.name | highlight:searchText"></span> <mat-option [value]="entityInfo">
</mat-option> <span [innerHTML]="entityInfo.name | highlight:searchText"></span>
<mat-option *ngIf="!(filteredEntityInfos | async)?.length" [value]="null"> </mat-option>
<span> }
@if (!(filteredEntityInfos | async)?.length) {
<mat-option [value]="null">
<span>
{{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }} {{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }}
</span> </span>
</mat-option> </mat-option>
}
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>

20
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select-panel.component.html

@ -16,13 +16,15 @@
--> -->
<div class="mat-content mat-padding flex flex-col"> <div class="mat-content mat-padding flex flex-col">
<div class="flex flex-row" *ngFor="let alias of entityAliasesInfo | keyvalue"> @for (alias of entityAliasesInfo | keyvalue; track alias) {
<tb-aliases-entity-autocomplete class="flex-1" <div class="flex flex-row">
required <tb-aliases-entity-autocomplete class="flex-1"
[alias]="alias.value.alias" required
[entityFilter]="alias.value.entityFilter" [alias]="alias.value.alias"
[(ngModel)]="alias.value.currentEntity" [entityFilter]="alias.value.entityFilter"
(ngModelChange)="currentAliasEntityChanged(alias.key, alias.value.currentEntity)"> [(ngModel)]="alias.value.currentEntity"
</tb-aliases-entity-autocomplete> (ngModelChange)="currentAliasEntityChanged(alias.key, alias.value.currentEntity)">
</div> </tb-aliases-entity-autocomplete>
</div>
}
</div> </div>

26
ui-ngx/src/app/modules/home/components/alias/aliases-entity-select.component.html

@ -15,18 +15,20 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-aliases-entity-select flex flex-row items-center justify-start" *ngIf="hasSelectableAliasEntities"> @if (hasSelectableAliasEntities) {
<button mat-icon-button <section class="tb-aliases-entity-select flex flex-row items-center justify-start">
cdkOverlayOrigin #aliasEntitySelectPanelOrigin="cdkOverlayOrigin" <button mat-icon-button
cdkOverlayOrigin #aliasEntitySelectPanelOrigin="cdkOverlayOrigin"
(click)="openEditMode()"
matTooltip="{{ 'entity.select-entities' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">devices_other</mat-icon>
</button>
<span class="lt-lg:!hidden"
(click)="openEditMode()" (click)="openEditMode()"
matTooltip="{{ 'entity.select-entities' | translate }}" matTooltip="{{ 'entity.select-entities' | translate }}"
[matTooltipPosition]="tooltipPosition"> [matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">devices_other</mat-icon> {{displayValue}}
</button> </span>
<span class="lt-lg:!hidden" </section>
(click)="openEditMode()" }
matTooltip="{{ 'entity.select-entities' | translate }}"
[matTooltipPosition]="tooltipPosition">
{{displayValue}}
</span>
</section>

22
ui-ngx/src/app/modules/home/components/alias/entity-alias-dialog.component.html

@ -25,20 +25,26 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content> <div mat-dialog-content>
<fieldset class="tb-form-panel outlined" [disabled]="isLoading$ | async"> <fieldset class="tb-form-panel outlined" [disabled]="isLoading$ | async">
<div class="flex flex-row items-start xs:flex-col xs:items-stretch"> <div class="flex flex-row items-start xs:flex-col xs:items-stretch">
<mat-form-field class="flex-1" appearance="outline"> <mat-form-field class="flex-1" appearance="outline">
<mat-label translate>alias.name</mat-label> <mat-label translate>alias.name</mat-label>
<input matInput formControlName="alias" required> <input matInput formControlName="alias" required>
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('required')"> @if (entityAliasFormGroup.get('alias').hasError('required')) {
{{ 'alias.name-required' | translate }} <mat-error>
</mat-error> {{ 'alias.name-required' | translate }}
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('duplicateAliasName')"> </mat-error>
{{ 'alias.duplicate-alias' | translate }} }
</mat-error> @if (entityAliasFormGroup.get('alias').hasError('duplicateAliasName')) {
<mat-error>
{{ 'alias.duplicate-alias' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<div class="tb-resolve-multiple-switch" tb-hint-tooltip-icon="{{ 'alias.resolve-multiple-hint' | translate }}"> <div class="tb-resolve-multiple-switch" tb-hint-tooltip-icon="{{ 'alias.resolve-multiple-hint' | translate }}">
<mat-slide-toggle class="mat-slide" formControlName="resolveMultiple"> <mat-slide-toggle class="mat-slide" formControlName="resolveMultiple">

90
ui-ngx/src/app/modules/home/components/alias/entity-aliases-dialog.component.html

@ -25,9 +25,13 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div> </mat-progress-bar>
}
@if ((isLoading$ | async) === false) {
<div style="height: 4px;"></div>
}
<div mat-dialog-content> <div mat-dialog-content>
<div class="tb-aliases-header flex flex-row items-center justify-start"> <div class="tb-aliases-header flex flex-row items-center justify-start">
<span class="max-w-5% flex-full"></span> <span class="max-w-5% flex-full"></span>
@ -40,45 +44,49 @@
</div> </div>
<fieldset [disabled]="isLoading$ | async" class="tb-form-panel outlined"> <fieldset [disabled]="isLoading$ | async" class="tb-form-panel outlined">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div class="flex flex-1 flex-row items-center justify-start" @for (entityAliasControl of entityAliasesFormArray().controls; track entityAliasControl) {
formArrayName="entityAliases" <div class="flex flex-1 flex-row items-center justify-start"
*ngFor="let entityAliasControl of entityAliasesFormArray().controls; let $index = index"> formArrayName="entityAliases"
<span class="max-w-5% flex-full">{{$index + 1}}.</span> >
<div class="mat-elevation-z4 tb-alias flex max-w-95% flex-full flex-row items-center justify-start"> <span class="max-w-5% flex-full">{{$index + 1}}.</span>
<mat-form-field class="mat-block flex-1 gt-sm:min-w-52 gt-sm:max-w-52 gt-sm:basis-52" style="padding-top: 20px;" appearance="outline"> <div class="mat-elevation-z4 tb-alias flex max-w-95% flex-full flex-row items-center justify-start">
<input matInput [formControl]="entityAliasControl.get('alias')" required placeholder="{{ 'entity.alias' | translate }}"> <mat-form-field class="mat-block flex-1 gt-sm:min-w-52 gt-sm:max-w-52 gt-sm:basis-52" style="padding-top: 20px;" appearance="outline">
<mat-error *ngIf="entityAliasControl.get('alias').hasError('required')"> <input matInput [formControl]="entityAliasControl.get('alias')" required placeholder="{{ 'entity.alias' | translate }}">
{{ 'entity.alias-required' | translate }} @if (entityAliasControl.get('alias').hasError('required')) {
</mat-error> <mat-error>
</mat-form-field> {{ 'entity.alias-required' | translate }}
<tb-entity-filter-view class="flex-1" style="padding-left: 10px;" [formControl]="entityAliasControl.get('filter')"> </mat-error>
</tb-entity-filter-view> }
<section style="padding-left: 10px;" </mat-form-field>
class="tb-resolve-multiple-switch flex min-w-32 max-w-32 flex-1 basis-32 flex-col items-center justify-center"> <tb-entity-filter-view class="flex-1" style="padding-left: 10px;" [formControl]="entityAliasControl.get('filter')">
<mat-slide-toggle class="resolve-multiple-switch" </tb-entity-filter-view>
[formControl]="entityAliasControl.get('resolveMultiple')"> <section style="padding-left: 10px;"
</mat-slide-toggle> class="tb-resolve-multiple-switch flex min-w-32 max-w-32 flex-1 basis-32 flex-col items-center justify-center">
</section> <mat-slide-toggle class="resolve-multiple-switch"
<button [disabled]="isLoading$ | async" [formControl]="entityAliasControl.get('resolveMultiple')">
mat-icon-button color="primary" </mat-slide-toggle>
style="min-width: 40px;" </section>
type="button" <button [disabled]="isLoading$ | async"
(click)="editAlias($index)" mat-icon-button color="primary"
matTooltip="{{ 'alias.edit' | translate }}" style="min-width: 40px;"
matTooltipPosition="above"> type="button"
<mat-icon>edit</mat-icon> (click)="editAlias($index)"
</button> matTooltip="{{ 'alias.edit' | translate }}"
<button [disabled]="isLoading$ | async" matTooltipPosition="above">
mat-icon-button color="primary" <mat-icon>edit</mat-icon>
style="min-width: 40px;" </button>
type="button" <button [disabled]="isLoading$ | async"
(click)="removeAlias($index)" mat-icon-button color="primary"
matTooltip="{{ 'entity.remove-alias' | translate }}" style="min-width: 40px;"
matTooltipPosition="above"> type="button"
<mat-icon>close</mat-icon> (click)="removeAlias($index)"
</button> matTooltip="{{ 'entity.remove-alias' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div> </div>
</div> }
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>

8
ui-ngx/src/app/modules/home/components/api-key/add-api-key-dialog.component.html

@ -36,9 +36,11 @@
<mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="mat-block" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>api-key.description</mat-label> <mat-label translate>api-key.description</mat-label>
<textarea cdkTextareaAutosize matInput formControlName="description" rows="2" maxLength="255"></textarea> <textarea cdkTextareaAutosize matInput formControlName="description" rows="2" maxLength="255"></textarea>
<mat-error *ngIf="apiKeyForm.get('description').hasError('required')"> @if (apiKeyForm.get('description').hasError('required')) {
{{ 'asset.description-required' | translate }} <mat-error>
</mat-error> {{ 'asset.description-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<mat-slide-toggle class="mat-slide" formControlName="enabled"> <mat-slide-toggle class="mat-slide" formControlName="enabled">
{{ 'api-key.enable' | translate }} {{ 'api-key.enable' | translate }}

8
ui-ngx/src/app/modules/home/components/api-key/edit-api-key-description-panel.component.html

@ -20,9 +20,11 @@
<mat-form-field class="mat-block" appearance="outline"> <mat-form-field class="mat-block" appearance="outline">
<mat-label translate>api-key.description</mat-label> <mat-label translate>api-key.description</mat-label>
<textarea #input cdkTextareaAutosize matInput [formControl]="descriptionFormControl" rows="2" maxLength="255"></textarea> <textarea #input cdkTextareaAutosize matInput [formControl]="descriptionFormControl" rows="2" maxLength="255"></textarea>
<mat-error *ngIf="descriptionFormControl.hasError('required')"> @if (descriptionFormControl.hasError('required')) {
{{ 'asset.description-required' | translate }} <mat-error>
</mat-error> {{ 'asset.description-required' | translate }}
</mat-error>
}
<mat-hint align="end">{{ input.value?.length || 0 }}/255</mat-hint> <mat-hint align="end">{{ input.value?.length || 0 }}/255</mat-hint>
</mat-form-field> </mat-form-field>
<div class="tb-edit-api-key-description-panel-buttons"> <div class="tb-edit-api-key-description-panel-buttons">

9
ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html

@ -25,9 +25,12 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> </mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col gap-2"> <fieldset [disabled]="isLoading$ | async" class="flex flex-col gap-2">
<tb-string-autocomplete <tb-string-autocomplete

14
ui-ngx/src/app/modules/home/components/attribute/add-widget-to-dashboard-dialog.component.html

@ -25,8 +25,10 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content class="flex flex-col"> <div mat-dialog-content class="flex flex-col">
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<mat-radio-group formControlName="addToDashboardType"> <mat-radio-group formControlName="addToDashboardType">
@ -46,9 +48,11 @@
<mat-label translate>dashboard.new-dashboard-title</mat-label> <mat-label translate>dashboard.new-dashboard-title</mat-label>
<input matInput formControlName="newDashboardTitle" <input matInput formControlName="newDashboardTitle"
[required]="addWidgetFormGroup.get('addToDashboardType').value === 1"> [required]="addWidgetFormGroup.get('addToDashboardType').value === 1">
<mat-error *ngIf="addWidgetFormGroup.get('newDashboardTitle').hasError('required')"> @if (addWidgetFormGroup.get('newDashboardTitle').hasError('required')) {
{{ 'dashboard.title-required' | translate }} <mat-error>
</mat-error> {{ 'dashboard.title-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
</section> </section>
</mat-radio-button> </mat-radio-button>

220
ui-ngx/src/app/modules/home/components/attribute/attribute-table.component.html

@ -21,34 +21,40 @@
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<div class="title-container flex flex-row items-center justify-start xs:flex-col xs:items-start xs:justify-center"> <div class="title-container flex flex-row items-center justify-start xs:flex-col xs:items-start xs:justify-center">
<span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span> <span class="tb-entity-table-title">{{telemetryTypeTranslationsMap.get(attributeScope) | translate}}</span>
<mat-form-field class="mat-block tb-attribute-scope" *ngIf="!disableAttributeScopeSelection && !attributeScopeSelectionReadonly"> @if (!disableAttributeScopeSelection && !attributeScopeSelectionReadonly) {
<mat-label translate>attribute.attributes-scope</mat-label> <mat-form-field class="mat-block tb-attribute-scope">
<mat-select [disabled]="(isLoading$ | async)" <mat-label translate>attribute.attributes-scope</mat-label>
[ngModel]="attributeScope" <mat-select [disabled]="(isLoading$ | async)"
(ngModelChange)="attributeScopeChanged($event)"> [ngModel]="attributeScope"
<mat-option *ngFor="let scope of attributeScopes" [value]="scope"> (ngModelChange)="attributeScopeChanged($event)">
{{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }} @for (scope of attributeScopes; track scope) {
</mat-option> <mat-option [value]="scope">
</mat-select> {{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }}
</mat-form-field> </mat-option>
}
</mat-select>
</mat-form-field>
}
</div> </div>
<span class="flex-1"></span> <span class="flex-1"></span>
<button mat-icon-button @if (attributeScope !== attributeScopeTypes.CLIENT_SCOPE) {
*ngIf="attributeScope !== attributeScopeTypes.CLIENT_SCOPE" <button mat-icon-button
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
(click)="addAttribute($event)" (click)="addAttribute($event)"
matTooltip="{{ 'action.add' | translate }}" matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
<button mat-icon-button }
*ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)" @if (!isClientSideTelemetryTypeMap.get(attributeScope)) {
[disabled]="isLoading$ | async" <button mat-icon-button
(click)="reloadAttributes()" [disabled]="isLoading$ | async"
matTooltip="{{ 'action.refresh' | translate }}" (click)="reloadAttributes()"
matTooltipPosition="above"> matTooltip="{{ 'action.refresh' | translate }}"
<mat-icon>refresh</mat-icon> matTooltipPosition="above">
</button> <mat-icon>refresh</mat-icon>
</button>
}
<button mat-icon-button <button mat-icon-button
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
(click)="enterFilterMode()" (click)="enterFilterMode()"
@ -82,7 +88,7 @@
<div class="mat-toolbar-tools xs:flex xs:flex-row xs:flex-wrap"> <div class="mat-toolbar-tools xs:flex xs:flex-row xs:flex-wrap">
<span class="tb-entity-table-info"> <span class="tb-entity-table-info">
{{ (attributeScope === latestTelemetryTypes.LATEST_TELEMETRY ? {{ (attributeScope === latestTelemetryTypes.LATEST_TELEMETRY ?
'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }} 'attribute.selected-telemetry' : 'attribute.selected-attributes') | translate:{count: dataSource.selection.selected.length} }}
</span> </span>
<span class="flex-1"></span> <span class="flex-1"></span>
<button [class.!hidden]="attributeScope === attributeScopeTypes.CLIENT_SCOPE" <button [class.!hidden]="attributeScope === attributeScopeTypes.CLIENT_SCOPE"
@ -139,7 +145,7 @@
</mat-toolbar> </mat-toolbar>
<div class="table-container flex-1" [class.!hidden]="mode === 'widget'"> <div class="table-container flex-1" [class.!hidden]="mode === 'widget'">
<table mat-table [dataSource]="dataSource" <table mat-table [dataSource]="dataSource"
matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> matSort [matSortActive]="pageLink.sortOrder.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="select" sticky> <ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 40px;"> <mat-header-cell *matHeaderCellDef style="width: 40px;">
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null" <mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
@ -173,7 +179,7 @@
tooltipPosition="above" tooltipPosition="above"
icon="content_copy" icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}" [style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
> >
</tb-copy-button> </tb-copy-button>
</div> </div>
</mat-cell> </mat-cell>
@ -186,38 +192,41 @@
<div class="flex flex-1 flex-row" style="align-items: center"> <div class="flex flex-1 flex-row" style="align-items: center">
<div class="flex flex-1 flex-row" style="overflow: hidden; align-items: center;"> <div class="flex flex-1 flex-row" style="overflow: hidden; align-items: center;">
<span class="ellipsis">{{attribute.value | tbJson}}</span> <span class="ellipsis">{{attribute.value | tbJson}}</span>
<tb-copy-button @if (attribute.value !== '') {
*ngIf="attribute.value !== ''" <tb-copy-button
class="attribute-copy" class="attribute-copy"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
[copyText]="attribute.value | tbJson" [copyText]="attribute.value | tbJson"
tooltipText="{{ 'attribute.copy-value' | translate }}" tooltipText="{{ 'attribute.copy-value' | translate }}"
tooltipPosition="above" tooltipPosition="above"
icon="content_copy" icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}" [style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
> >
</tb-copy-button> </tb-copy-button>
}
</div> </div>
<span [class.!hidden]="isClientSideTelemetryTypeMap.get(attributeScope)"> <span [class.!hidden]="isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</span> </span>
<button mat-icon-button *ngIf="attributeScope === latestTelemetryTypes.LATEST_TELEMETRY" @if (attributeScope === latestTelemetryTypes.LATEST_TELEMETRY) {
(click)="deleteTimeseries($event, attribute)"> <button mat-icon-button
<mat-icon>delete</mat-icon> (click)="deleteTimeseries($event, attribute)">
</button> <mat-icon>delete</mat-icon>
</button>
}
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> <mat-header-row class="mat-row-select" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
<mat-row class="mat-row-select" [class.mat-selected]="dataSource.selection.isSelected(attribute)" <mat-row class="mat-row-select" [class.mat-selected]="dataSource.selection.isSelected(attribute)"
*matRowDef="let attribute; columns: displayedColumns;" (click)="dataSource.selection.toggle(attribute)"></mat-row> *matRowDef="let attribute; columns: displayedColumns;" (click)="dataSource.selection.toggle(attribute)"></mat-row>
</table> </table>
<span [class.!hidden]="(dataSource.isEmpty() | async) === false" <span [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="no-data-found flex items-center justify-center" translate>{{ class="no-data-found flex items-center justify-center" translate>{{
attributeScope === latestTelemetryTypes.LATEST_TELEMETRY attributeScope === latestTelemetryTypes.LATEST_TELEMETRY
? 'attribute.no-telemetry-text' ? 'attribute.no-telemetry-text'
: 'attribute.no-attributes-text' : 'attribute.no-attributes-text'
}}</span> }}</span>
</div> </div>
<mat-divider [class.!hidden]="mode === 'widget'"></mat-divider> <mat-divider [class.!hidden]="mode === 'widget'"></mat-divider>
<mat-paginator [class.!hidden]="mode === 'widget'" <mat-paginator [class.!hidden]="mode === 'widget'"
@ -226,58 +235,65 @@
[pageSize]="pageLink.pageSize" [pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]" [pageSizeOptions]="[10, 20, 30]"
[hidePageSize]="hidePageSize" [hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator> showFirstLastButtons></mat-paginator>
<ngx-hm-carousel *ngIf="mode === 'widget' && widgetsList.length > 0" @if (mode === 'widget' && widgetsList.length > 0) {
#carousel <ngx-hm-carousel
[(ngModel)]="widgetsCarouselIndex" #carousel
(ngModelChange)="onWidgetsCarouselIndexChanged()" [(ngModel)]="widgetsCarouselIndex"
[data]="widgetsList" (ngModelChange)="onWidgetsCarouselIndexChanged()"
[infinite]="false" [data]="widgetsList"
class="carousel c-accent flex-1"> [infinite]="false"
<section ngx-hm-carousel-container class="content"> class="carousel c-accent flex-1">
<article class="item cursor-pointer" <section ngx-hm-carousel-container class="content">
ngx-hm-carousel-item @for (widgets of widgetsList; track widgets) {
*ngFor="let widgets of widgetsList"> <article class="item cursor-pointer"
<tb-dashboard class="tb-absolute-fill" ngx-hm-carousel-item
[aliasController]="aliasController" >
[widgets]="widgets" <tb-dashboard class="tb-absolute-fill"
[columns]="20" [aliasController]="aliasController"
[isEdit]="false" [widgets]="widgets"
[isMobileDisabled]="true" [columns]="20"
[isEditActionEnabled]="false" [isEdit]="false"
[isRemoveActionEnabled]="false"> [isMobileDisabled]="true"
</tb-dashboard> [isEditActionEnabled]="false"
</article> [isRemoveActionEnabled]="false">
</section> </tb-dashboard>
</article>
<ng-template #carouselPrev> }
<button mat-icon-button </section>
*ngIf="widgetsCarouselIndex > 0" <ng-template #carouselPrev>
matTooltip="{{ 'attribute.prev-widget' | translate }}" @if (widgetsCarouselIndex > 0) {
matTooltipPosition="above"> <button mat-icon-button
<mat-icon>keyboard_arrow_left</mat-icon> matTooltip="{{ 'attribute.prev-widget' | translate }}"
</button> matTooltipPosition="above">
</ng-template> <mat-icon>keyboard_arrow_left</mat-icon>
<ng-template #carouselNext> </button>
<button mat-icon-button }
*ngIf="widgetsCarouselIndex < widgetsList.length - 1" </ng-template>
matTooltip="{{ 'attribute.next-widget' | translate }}" <ng-template #carouselNext>
matTooltipPosition="above"> @if (widgetsCarouselIndex < widgetsList.length - 1) {
<mat-icon>keyboard_arrow_right</mat-icon> <button mat-icon-button
</button> matTooltip="{{ 'attribute.next-widget' | translate }}"
</ng-template> matTooltipPosition="above">
<mat-icon>keyboard_arrow_right</mat-icon>
<ng-template #carouselDot let-model> </button>
<div class="ball" }
[class.visible]="model.index === model.currentIndex"></div> </ng-template>
</ng-template> <ng-template #carouselDot let-model>
<div class="ball"
</ngx-hm-carousel> [class.visible]="model.index === model.currentIndex"></div>
<span translate *ngIf="mode === 'widget' && widgetsLoaded && </ng-template>
widgetsList.length === 0 && </ngx-hm-carousel>
widgetBundleSet" }
class="mat-headline-5 flex flex-1 items-center justify-center">widgets-bundle.empty</span> @if (mode === 'widget' && widgetsLoaded &&
<span translate *ngIf="mode === 'widget' && !widgetBundleSet" widgetsList.length === 0 &&
class="mat-headline-5 flex flex-1 items-center justify-center">widget.select-widgets-bundle</span> widgetBundleSet) {
<span translate
class="mat-headline-5 flex flex-1 items-center justify-center">widgets-bundle.empty</span>
}
@if (mode === 'widget' && !widgetBundleSet) {
<span translate
class="mat-headline-5 flex flex-1 items-center justify-center">widget.select-widgets-bundle</span>
}
</div> </div>
</div> </div>

46
ui-ngx/src/app/modules/home/components/attribute/delete-timeseries-panel.component.html

@ -28,28 +28,34 @@
<mat-form-field class="mat-block" subscriptSizing="dynamic"> <mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>attribute.delete-timeseries.strategy</mat-label> <mat-label translate>attribute.delete-timeseries.strategy</mat-label>
<mat-select formControlName="strategy"> <mat-select formControlName="strategy">
<mat-option *ngFor="let strategy of strategiesTranslationsMap.keys()" [value]="strategy"> @for (strategy of strategiesTranslationsMap.keys(); track strategy) {
{{ strategiesTranslationsMap.get(strategy) | translate }} <mat-option [value]="strategy">
</mat-option> {{ strategiesTranslationsMap.get(strategy) | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div *ngIf="isPeriodStrategy()" class="tb-select-interval"> @if (isPeriodStrategy()) {
<mat-form-field class="mat-block" subscriptSizing="dynamic"> <div class="tb-select-interval">
<mat-label translate>attribute.delete-timeseries.start-time</mat-label> <mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-datetimepicker-toggle [for]="startDateTimePicker" matPrefix></mat-datetimepicker-toggle> <mat-label translate>attribute.delete-timeseries.start-time</mat-label>
<mat-datetimepicker #startDateTimePicker type="datetime" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker-toggle [for]="startDateTimePicker" matPrefix></mat-datetimepicker-toggle>
<input required matInput formControlName="startDateTime" [matDatetimepicker]="startDateTimePicker"> <mat-datetimepicker #startDateTimePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
</mat-form-field> <input required matInput formControlName="startDateTime" [matDatetimepicker]="startDateTimePicker">
<mat-form-field class="mat-block" subscriptSizing="dynamic"> </mat-form-field>
<mat-label translate>attribute.delete-timeseries.ends-on</mat-label> <mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle> <mat-label translate>attribute.delete-timeseries.ends-on</mat-label>
<mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
<input required matInput formControlName="endDateTime" [matDatetimepicker]="endDatePicker"> <mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
</mat-form-field> <input required matInput formControlName="endDateTime" [matDatetimepicker]="endDatePicker">
</div> </mat-form-field>
<mat-slide-toggle class="tb-slide-toggle" formControlName="rewriteLatest" *ngIf="isPeriodStrategy() || isDeleteLatestStrategy()"> </div>
{{ "attribute.delete-timeseries.rewrite-latest-value" | translate }} }
</mat-slide-toggle> @if (isPeriodStrategy() || isDeleteLatestStrategy()) {
<mat-slide-toggle class="tb-slide-toggle" formControlName="rewriteLatest">
{{ "attribute.delete-timeseries.rewrite-latest-value" | translate }}
</mat-slide-toggle>
}
</form> </form>
<div class="tb-panel-actions flex flex-row"> <div class="tb-panel-actions flex flex-row">
<span class="flex-1"></span> <span class="flex-1"></span>

180
ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.component.html

@ -15,26 +15,28 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-details-buttons xs:flex xs:flex-col" *ngIf="!standalone"> @if (!standalone) {
<button mat-raised-button color="primary" <div class="tb-details-buttons xs:flex xs:flex-col">
[disabled]="(isLoading$ | async)" <button mat-raised-button color="primary"
(click)="onEntityAction($event, 'open')" [disabled]="(isLoading$ | async)"
[class.!hidden]="isEdit || isDetailsPage"> (click)="onEntityAction($event, 'open')"
{{ 'common.open-details-page' | translate }} [class.!hidden]="isEdit || isDetailsPage">
</button> {{ 'common.open-details-page' | translate }}
<button mat-raised-button color="primary" </button>
[disabled]="(isLoading$ | async)" <button mat-raised-button color="primary"
(click)="onEntityAction($event, 'export')" [disabled]="(isLoading$ | async)"
[class.!hidden]="isEdit"> (click)="onEntityAction($event, 'export')"
{{ 'action.export' | translate }} [class.!hidden]="isEdit">
</button> {{ 'action.export' | translate }}
<button mat-raised-button color="primary" </button>
[disabled]="(isLoading$ | async)" <button mat-raised-button color="primary"
(click)="onEntityAction($event, 'delete')" [disabled]="(isLoading$ | async)"
[class.!hidden]="hideDelete() || isEdit"> (click)="onEntityAction($event, 'delete')"
{{ 'action.delete' | translate }} [class.!hidden]="hideDelete() || isEdit">
</button> {{ 'action.delete' | translate }}
</div> </button>
</div>
}
<div class="mat-padding flex flex-1 flex-col" [formGroup]="entityForm"> <div class="mat-padding flex flex-1 flex-col" [formGroup]="entityForm">
<div class="tb-form-panel no-border no-padding"> <div class="tb-form-panel no-border no-padding">
<div class="tb-form-panel no-gap"> <div class="tb-form-panel no-gap">
@ -55,11 +57,11 @@
<mat-hint></mat-hint> <mat-hint></mat-hint>
</mat-form-field> </mat-form-field>
<tb-entity-debug-settings-button <tb-entity-debug-settings-button
formControlName="debugSettings" formControlName="debugSettings"
style="margin-bottom: 22px" style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD" [entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig" [additionalActionConfig]="additionalDebugActionConfig"
/> />
</div> </div>
<tb-entity-select formControlName="entityId" <tb-entity-select formControlName="entityId"
appearance="outline" appearance="outline"
@ -74,8 +76,8 @@
<mat-select formControlName="type"> <mat-select formControlName="type">
<mat-select-trigger> <mat-select-trigger>
{{ CalculatedFieldTypeTranslations.has(entityForm.get('type').value) {{ CalculatedFieldTypeTranslations.has(entityForm.get('type').value)
? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate) ? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate)
: entityForm.get('type').value }} : entityForm.get('type').value }}
</mat-select-trigger> </mat-select-trigger>
@for (type of fieldTypes; track type) { @for (type of fieldTypes; track type) {
<mat-option [value]="type" class="py-1"> <mat-option [value]="type" class="py-1">
@ -83,67 +85,67 @@
{{ CalculatedFieldTypeTranslations.get(type).name | translate }} {{ CalculatedFieldTypeTranslations.get(type).name | translate }}
</span> </span>
<br> <br>
<small matListItemMeta style="color: inherit"> <small matListItemMeta style="color: inherit">
{{ CalculatedFieldTypeTranslations.get(type).hint | translate }} {{ CalculatedFieldTypeTranslations.get(type).hint | translate }}
</small> </small>
</mat-option> </mat-option>
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@switch (entityForm.get('type').value) { @switch (entityForm.get('type').value) {
@case (CalculatedFieldType.GEOFENCING) { @case (CalculatedFieldType.GEOFENCING) {
<tb-geofencing-configuration formControlName="configuration" <tb-geofencing-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId" [entityId]="entityId"
[entityName]="entityName" [entityName]="entityName"
[ownerId]="ownerId" [ownerId]="ownerId"
[tenantId]="tenantId" [tenantId]="tenantId"
[isEditValue]="isEditValue" [isEditValue]="isEditValue"
></tb-geofencing-configuration> ></tb-geofencing-configuration>
} }
@case (CalculatedFieldType.PROPAGATION) { @case (CalculatedFieldType.PROPAGATION) {
<tb-propagation-configuration formControlName="configuration" <tb-propagation-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId" [entityId]="entityId"
[entityName]="entityName" [entityName]="entityName"
[tenantId]="tenantId" [tenantId]="tenantId"
[ownerId]="ownerId" [ownerId]="ownerId"
[testScript]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue" [isEditValue]="isEditValue"
></tb-propagation-configuration> ></tb-propagation-configuration>
} }
@case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) {
<tb-related-entities-aggregation-component formControlName="configuration" <tb-related-entities-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId" [entityId]="entityId"
[entityName]="entityName" [entityName]="entityName"
[tenantId]="tenantId" [tenantId]="tenantId"
[testScript]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue" [isEditValue]="isEditValue"
></tb-related-entities-aggregation-component> ></tb-related-entities-aggregation-component>
} }
@case (CalculatedFieldType.ENTITY_AGGREGATION) { @case (CalculatedFieldType.ENTITY_AGGREGATION) {
<tb-entity-aggregation-component formControlName="configuration" <tb-entity-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId" [entityId]="entityId"
[entityName]="entityName" [entityName]="entityName"
[tenantId]="tenantId" [tenantId]="tenantId"
[isEditValue]="isEditValue" [isEditValue]="isEditValue"
></tb-entity-aggregation-component> ></tb-entity-aggregation-component>
} }
@default { @default {
<tb-simple-configuration formControlName="configuration" <tb-simple-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration" [class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId" [entityId]="entityId"
[entityName]="entityName" [entityName]="entityName"
[ownerId]="ownerId" [ownerId]="ownerId"
[tenantId]="tenantId" [tenantId]="tenantId"
[isScript]="entityForm.get('type').value === CalculatedFieldType.SCRIPT" [isScript]="entityForm.get('type').value === CalculatedFieldType.SCRIPT"
[testScript]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue" [isEditValue]="isEditValue"
></tb-simple-configuration> ></tb-simple-configuration>
}
} }
} </div>
</div> </div>
</div>

12
ui-ngx/src/app/modules/home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.component.html

@ -48,9 +48,11 @@
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>calculated-fields.aggregate-interval-type</mat-label> <mat-label translate>calculated-fields.aggregate-interval-type</mat-label>
<mat-select formControlName="type" required> <mat-select formControlName="type" required>
<mat-option *ngFor="let type of AggIntervalTypes" [value]="type"> @for (type of AggIntervalTypes; track type) {
{{ AggIntervalTypeTranslations.get(type) | translate }} <mat-option [value]="type">
</mat-option> {{ AggIntervalTypeTranslations.get(type) | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<tb-timezone-select <tb-timezone-select
@ -128,7 +130,7 @@
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide flex" formControlName="produceIntermediateResult"> <mat-slide-toggle class="mat-slide flex" formControlName="produceIntermediateResult">
<div tb-hint-tooltip-icon="{{ 'calculated-fields.entity-aggregation.produce-intermediate-result-hint' | translate: { <div tb-hint-tooltip-icon="{{ 'calculated-fields.entity-aggregation.produce-intermediate-result-hint' | translate: {
time: (intermediateAggregationIntervalInSecForCF * 1000 | milliSecondsToTimeString: false: true ) time: (intermediateAggregationIntervalInSecForCF * 1000 | milliSecondsToTimeString: false: true )
} }}"> } }}">
{{ 'calculated-fields.entity-aggregation.produce-intermediate-result' | translate }} {{ 'calculated-fields.entity-aggregation.produce-intermediate-result' | translate }}
</div> </div>
@ -140,5 +142,5 @@
simpleMode simpleMode
hiddenName hiddenName
disableType disableType
[entityId]="entityId"></tb-calculate-field-output> [entityId]="entityId"></tb-calculate-field-output>
</div> </div>

168
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-panel.component.html

@ -93,90 +93,92 @@
} }
</ng-container> </ng-container>
<ng-container [formGroup]="refDynamicSourceFormGroup"> <ng-container [formGroup]="refDynamicSourceFormGroup">
<div class="tb-form-panel stroked" *ngIf="entityType === ArgumentEntityType.RelationQuery"> @if (entityType === ArgumentEntityType.RelationQuery) {
<mat-expansion-panel class="tb-settings" expanded> <div class="tb-form-panel stroked">
<mat-expansion-panel-header> <mat-expansion-panel class="tb-settings" expanded>
<span class="tb-form-panel-title tb-required">{{ 'calculated-fields.entity-zone-relationship' | translate }}</span> <mat-expansion-panel-header>
</mat-expansion-panel-header> <span class="tb-form-panel-title tb-required">{{ 'calculated-fields.entity-zone-relationship' | translate }}</span>
<div class="tb-form-table"> </mat-expansion-panel-header>
<div class="tb-form-table-header"> <div class="tb-form-table">
<div class="tb-form-table-header-cell tb-actions-header"></div> <div class="tb-form-table-header">
<div class="tb-form-table-header-cell" translate>calculated-fields.level</div> <div class="tb-form-table-header-cell tb-actions-header"></div>
<div class="tb-form-table-header-cell flex-1" translate>calculated-fields.direction-level</div> <div class="tb-form-table-header-cell" translate>calculated-fields.level</div>
<div class="tb-form-table-header-cell tb-required flex-1" translate>calculated-fields.relation-type</div> <div class="tb-form-table-header-cell flex-1" translate>calculated-fields.direction-level</div>
<div class="tb-form-table-header-cell tb-actions-header"></div> <div class="tb-form-table-header-cell tb-required flex-1" translate>calculated-fields.relation-type</div>
</div> <div class="tb-form-table-header-cell tb-actions-header"></div>
@if (levelsFormArray()?.controls?.length) {
<div class="tb-form-table-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)">
@for (keyControl of levelsFormArray().controls; track trackByKey; ) {
<div cdkDrag [cdkDragDisabled]="!dragEnabled" class="tb-draggable-form-table-row">
<div class="tb-form-table-row-cell-buttons">
<button mat-icon-button
type="button"
cdkDragHandle
class="lt-lg:!hidden"
[class.tb-hidden]="!dragEnabled"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
<div class="tb-form-row no-border flex-1" [formGroup]="keyControl">
<div class="level-text">{{ $index + 1 }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction">
@for (direction of GeofencingDirectionList; track direction) {
<mat-option [value]="direction">{{ GeofencingDirectionLevelTranslations.get(direction) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.relation-type-required' | translate"
formControlName="relationType">
</tb-string-autocomplete>
</div>
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
(click)="removeKey($index)"
[disabled]="readonly"
matTooltip="{{ 'calculated-fields.delete-level' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
}
</div>
} @else {
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-level' | translate }}</span>
}
@if (levelsFormArray().errors) {
<tb-error noMargin error="{{ 'calculated-fields.levels-required' | translate }}" style="padding-left: 12px;"></tb-error>
}
</div>
<div>
@if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) {
<div class="tb-form-hint tb-primary-fill max-args-warning flex items-center gap-2">
<mat-icon>warning</mat-icon>
<span>{{ 'calculated-fields.max-allowed-levels-error' | translate }}</span>
</div> </div>
} @else { @if (levelsFormArray()?.controls?.length) {
<button type="button" mat-stroked-button color="primary" (click)="addKey()" [disabled]="readonly"> <div class="tb-form-table-body tb-drop-list"
{{ 'calculated-fields.add-level' | translate }} cdkDropList cdkDropListOrientation="vertical"
</button> [cdkDropListDisabled]="!dragEnabled"
} (cdkDropListDropped)="keyDrop($event)">
</div> @for (keyControl of levelsFormArray().controls; track trackByKey; ) {
</mat-expansion-panel> <div cdkDrag [cdkDragDisabled]="!dragEnabled" class="tb-draggable-form-table-row">
</div> <div class="tb-form-table-row-cell-buttons">
<button mat-icon-button
type="button"
cdkDragHandle
class="lt-lg:!hidden"
[class.tb-hidden]="!dragEnabled"
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
<div class="tb-form-row no-border flex-1" [formGroup]="keyControl">
<div class="level-text">{{ $index + 1 }}</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="direction">
@for (direction of GeofencingDirectionList; track direction) {
<mat-option [value]="direction">{{ GeofencingDirectionLevelTranslations.get(direction) | translate }}</mat-option>
}
</mat-select>
</mat-form-field>
<tb-string-autocomplete [fetchOptionsFn]="fetchOptions.bind(this)"
additionalClass="tb-suffix-show-on-hover"
class="flex-1"
appearance="outline"
panelWidth=""
required
[errorText]="'calculated-fields.hint.relation-type-required' | translate"
formControlName="relationType">
</tb-string-autocomplete>
</div>
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
(click)="removeKey($index)"
[disabled]="readonly"
matTooltip="{{ 'calculated-fields.delete-level' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
}
</div>
} @else {
<span class="tb-prompt flex items-center justify-center">{{ 'calculated-fields.no-level' | translate }}</span>
}
@if (levelsFormArray().errors) {
<tb-error noMargin error="{{ 'calculated-fields.levels-required' | translate }}" style="padding-left: 12px;"></tb-error>
}
</div>
<div>
@if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) {
<div class="tb-form-hint tb-primary-fill max-args-warning flex items-center gap-2">
<mat-icon>warning</mat-icon>
<span>{{ 'calculated-fields.max-allowed-levels-error' | translate }}</span>
</div>
} @else {
<button type="button" mat-stroked-button color="primary" (click)="addKey()" [disabled]="readonly">
{{ 'calculated-fields.add-level' | translate }}
</button>
}
</div>
</mat-expansion-panel>
</div>
}
</ng-container> </ng-container>
<ng-container> <ng-container>
@if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Owner) { @if (entityFilter.singleEntity?.id || entityType === ArgumentEntityType.RelationQuery || entityType === ArgumentEntityType.Current || entityType === ArgumentEntityType.Owner) {

60
ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html

@ -15,11 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<ng-container *ngIf="panelMode; else componentMode"> @if (panelMode) {
<ng-container *ngTemplateOutlet="calculatedFieldsFilterPanel"></ng-container> <ng-container *ngTemplateOutlet="calculatedFieldsFilterPanel"></ng-container>
</ng-container> } @else {
<ng-template #componentMode> @if (buttonMode) {
<ng-container *ngIf="buttonMode; else calculatedFieldsFilter">
<button color="primary" <button color="primary"
matTooltip="{{buttonDisplayValue}}" matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above" matTooltipPosition="above"
@ -27,8 +26,49 @@
(click)="toggleCfFilterPanel($event)"> (click)="toggleCfFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}} <mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button> </button>
</ng-container> } @else {
</ng-template> <div class="tb-form-panel tb-calculated-fields-config-component no-padding no-border" [formGroup]="cfFilterForm">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>calculated-fields.calculated-field-types</div>
<tb-string-items-list subscriptSizing="dynamic"
formControlName="types"
appearance="outline"
fieldClass="flex"
class="flex-1"
placeholder="{{ !cfFilterForm.get('types').value?.length ? ('calculated-fields.any-type' | translate) : '' }}"
[predefinedValues]="types">
</tb-string-items-list>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>entity.entity-type</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
@for (type of listEntityTypes; track type) {
<mat-option [value]="type">
{{ entityTypeTranslations.get(type)?.type | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@if (cfFilterForm.get('entityType').value) {
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>entity.entities</div>
<tb-entity-list appearance="outline"
subscriptSizing="dynamic"
class="flex flex-1"
inlineField
syncIdsWithDB
labelText="{{'entity.entity-list' | translate}}"
[entityType]="cfFilterForm.get('entityType').value"
formControlName="entities">
</tb-entity-list>
</div>
}
</div>
}
}
<ng-template #calculatedFieldsFilterPanel> <ng-template #calculatedFieldsFilterPanel>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()"> <form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="calculatedFieldsFilter"></ng-container> <ng-container *ngTemplateOutlet="calculatedFieldsFilter"></ng-container>
@ -72,9 +112,11 @@
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}"> <mat-select formControlName="entityType" placeholder="{{ 'alarm-rule.any-type' | translate }}">
<mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option> <mat-option>{{ 'alarm-rule.any-type' | translate }}</mat-option>
<mat-option *ngFor="let type of listEntityTypes" [value]="type"> @for (type of listEntityTypes; track type) {
{{ entityTypeTranslations.get(type)?.type | translate }} <mat-option [value]="type">
</mat-option> {{ entityTypeTranslations.get(type)?.type | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

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

@ -21,11 +21,13 @@
<h2>{{'widget.add' | translate}}:</h2> <h2>{{'widget.add' | translate}}:</h2>
<span class="flex-1">{{data.widgetInfo.widgetName}}</span> <span class="flex-1">{{data.widgetInfo.widgetName}}</span>
<div class="flex flex-row items-center justify-start gt-xs:gap-4" style="min-width: fit-content;"> <div class="flex flex-row items-center justify-start gt-xs:gap-4" style="min-width: fit-content;">
<tb-toggle-select *ngIf="widgetConfig.hasBasicMode" appearance="fill-invert" selectMediaBreakpoint="xs" @if (widgetConfig.hasBasicMode) {
[(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}"> <tb-toggle-select appearance="fill-invert" selectMediaBreakpoint="xs"
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option> [(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option> <tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
</tb-toggle-select> <tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>
}
<div [tb-help]="helpLinkIdForWidgetType()"></div> <div [tb-help]="helpLinkIdForWidgetType()"></div>
</div> </div>
</div> </div>
@ -49,22 +51,24 @@
isAdd isAdd
formControlName="widgetConfig"> formControlName="widgetConfig">
</tb-widget-config> </tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill" @if (previewMode) {
[aliasController]="aliasController" <tb-widget-preview class="tb-absolute-fill"
[stateController]="stateController" [aliasController]="aliasController"
[dashboardTimewindow]="dashboard.configuration.timewindow" [stateController]="stateController"
[widget]="widget" [dashboardTimewindow]="dashboard.configuration.timewindow"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config" [widget]="widget"
[previewWidth]="widgetConfig.typeParameters.previewWidth" [widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewHeight]="widgetConfig.typeParameters.previewHeight"> [previewWidth]="widgetConfig.typeParameters.previewWidth"
<div class="tb-preview-panel-content"> [previewHeight]="widgetConfig.typeParameters.previewHeight">
<button mat-button <div class="tb-preview-panel-content">
(click)="previewMode = false"> <button mat-button
<mat-icon>chevron_left</mat-icon> (click)="previewMode = false">
{{ 'action.back' | translate }} <mat-icon>chevron_left</mat-icon>
</button> {{ 'action.back' | translate }}
</div> </button>
</tb-widget-preview> </div>
</tb-widget-preview>
}
</div> </div>
<div mat-dialog-actions class="flex items-center justify-between"> <div mat-dialog-actions class="flex items-center justify-between">
<button mat-button color="primary" <button mat-button color="primary"
@ -74,14 +78,16 @@
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<button *ngIf="!previewMode" @if (!previewMode) {
mat-button color="primary" <button
type="button" mat-button color="primary"
[disabled]="!widgetFormGroup.valid" type="button"
(click)="previewMode = true" [disabled]="!widgetFormGroup.valid"
cdkFocusInitial> (click)="previewMode = true"
{{ 'widget-config.preview' | translate }} cdkFocusInitial>
</button> {{ 'widget-config.preview' | translate }}
</button>
}
<button mat-raised-button color="primary" <button mat-raised-button color="primary"
(click)="add()" (click)="add()"
[disabled]="!widgetFormGroup.valid"> [disabled]="!widgetFormGroup.valid">

21
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-image-dialog.component.html

@ -25,8 +25,10 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content style="position: relative;"> <div mat-dialog-content style="position: relative;">
<fieldset [disabled]="(isLoading$ | async) || (takingScreenshot$ | async)" class="flex flex-col items-stretch justify-center gap-2"> <fieldset [disabled]="(isLoading$ | async) || (takingScreenshot$ | async)" class="flex flex-col items-stretch justify-center gap-2">
<div [formGroup]="dashboardRectFormGroup" class="flex flex-col items-center justify-center gap-2"> <div [formGroup]="dashboardRectFormGroup" class="flex flex-col items-center justify-center gap-2">
@ -40,8 +42,11 @@
<input matInput formControlName="left" type="number" step="1" min="0" max="100"> <input matInput formControlName="left" type="number" step="1" min="0" max="100">
</mat-form-field> </mat-form-field>
<div class="tb-image-preview-container"> <div class="tb-image-preview-container">
<div *ngIf="!imageUrl; else elseBlock">{{ 'dashboard.no-image' | translate }}</div> @if (!imageUrl) {
<ng-template #elseBlock><img class="tb-image-preview" [src]="imageUrl | image | async" /></ng-template> <div>{{ 'dashboard.no-image' | translate }}</div>
} @else {
<img class="tb-image-preview" [src]="imageUrl | image | async" />
}
</div> </div>
<mat-form-field class="rect-field"> <mat-form-field class="rect-field">
<mat-label>Right %</mat-label> <mat-label>Right %</mat-label>
@ -63,13 +68,15 @@
</div> </div>
<div [formGroup]="dashboardImageFormGroup"> <div [formGroup]="dashboardImageFormGroup">
<tb-gallery-image-input label="{{'dashboard.image' | translate}}" <tb-gallery-image-input label="{{'dashboard.image' | translate}}"
formControlName="dashboardImage"> formControlName="dashboardImage">
</tb-gallery-image-input> </tb-gallery-image-input>
</div> </div>
</fieldset> </fieldset>
<div *ngIf="takingScreenshot$ | async" class="taking-screenshot-progress tb-absolute-fill flex flex-col items-center justify-center"> @if (takingScreenshot$ | async) {
<div class="taking-screenshot-progress tb-absolute-fill flex flex-col items-center justify-center">
<mat-progress-spinner color="accent" mode="indeterminate"></mat-progress-spinner> <mat-progress-spinner color="accent" mode="indeterminate"></mat-progress-spinner>
</div> </div>
}
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" <button mat-button color="primary"

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

@ -25,9 +25,11 @@
[toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()"> [toolbarOpened]="toolbarOpened" (triggerClick)="openToolbar()">
<div class="tb-dashboard-action-panels flex flex-col items-stretch justify-center gt-sm:flex-row gt-sm:items-center gt-sm:justify-between"> <div class="tb-dashboard-action-panels flex flex-col items-stretch justify-center gt-sm:flex-row gt-sm:items-center gt-sm:justify-between">
<div class="tb-dashboard-action-panel tb-left-panel" [class.!hidden]="isMobileApp && !isEdit"> <div class="tb-dashboard-action-panel tb-left-panel" [class.!hidden]="isMobileApp && !isEdit">
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" class="gt-sm:!hidden" displayUserInfo="true"> @if (!isPublicUser() && forceFullscreen) {
</tb-user-menu> <tb-user-menu class="gt-sm:!hidden" displayUserInfo="true">
<ng-container *ngIf="isEdit"> </tb-user-menu>
}
@if (isEdit) {
<div style="flex: 1 1 100%" class="gt-sm:!hidden"></div> <div style="flex: 1 1 100%" class="gt-sm:!hidden"></div>
<div class="flex flex-row items-center justify-end lg:gap-1.5 lt-md:flex-1 lt-lg:gap-0.75 gt-sm:items-center gt-sm:justify-start gt-lg:gap-3"> <div class="flex flex-row items-center justify-end lg:gap-1.5 lt-md:flex-1 lt-lg:gap-0.75 gt-sm:items-center gt-sm:justify-start gt-lg:gap-3">
<mat-divider class="tb-toolbar-divider" vertical></mat-divider> <mat-divider class="tb-toolbar-divider" vertical></mat-divider>
@ -48,7 +50,7 @@
[dashboardCtrl]="this"> [dashboardCtrl]="this">
</tb-select-dashboard-breakpoint> </tb-select-dashboard-breakpoint>
</div> </div>
</ng-container> }
<tb-states-component class="lt-md:flex-1" <tb-states-component class="lt-md:flex-1"
[statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId" [statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
[dashboardCtrl]="this" [dashboardCtrl]="this"
@ -59,7 +61,7 @@
[syncStateWithQueryParam]="syncStateWithQueryParam" [syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states"> [states]="dashboardConfiguration.states">
</tb-states-component> </tb-states-component>
<ng-container *ngIf="isEdit"> @if (isEdit) {
<button class="gt-md:!hidden" mat-icon-button <button class="gt-md:!hidden" mat-icon-button
matTooltip="{{'dashboard.manage-states' | translate}}" matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below" matTooltipPosition="below"
@ -73,8 +75,10 @@
<mat-icon>layers</mat-icon> <mat-icon>layers</mat-icon>
{{'dashboard.states-short' | translate}} {{'dashboard.states-short' | translate}}
</button> </button>
</ng-container> }
<tb-logo *ngIf="showDashboardLogo()" [link]="dashboardLogoLink" [src]="dashboardLogo | image | async" class="dashboard_logo"></tb-logo> @if (showDashboardLogo()) {
<tb-logo [link]="dashboardLogoLink" [src]="dashboardLogo | image | async" class="dashboard_logo"></tb-logo>
}
</div> </div>
<div class="tb-dashboard-action-panel"> <div class="tb-dashboard-action-panel">
<button [class.!hidden]="!showCloseToolbar()" mat-icon-button class="close-action" <button [class.!hidden]="!showCloseToolbar()" mat-icon-button class="close-action"
@ -83,8 +87,10 @@
(click)="closeToolbar()"> (click)="closeToolbar()">
<mat-icon>arrow_forward</mat-icon> <mat-icon>arrow_forward</mat-icon>
</button> </button>
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" class="lt-md:!hidden" displayUserInfo="true"> @if (!isPublicUser() && forceFullscreen) {
</tb-user-menu> <tb-user-menu class="lt-md:!hidden" displayUserInfo="true">
</tb-user-menu>
}
<button [class.!hidden]="!showRightLayoutSwitch()" mat-icon-button <button [class.!hidden]="!showRightLayoutSwitch()" mat-icon-button
matTooltip="{{ (isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}" matTooltip="{{ (isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
@ -103,371 +109,399 @@
(click)="exportDashboard($event)"> (click)="exportDashboard($event)">
<mat-icon>file_download</mat-icon> <mat-icon>file_download</mat-icon>
</button> </button>
<ng-container *ngIf="!readonly && !isEdit"> @if (!readonly && !isEdit) {
<button class="gt-md:!hidden" mat-icon-button (click)="toggleDashboardEditMode()" <button class="gt-md:!hidden" mat-icon-button (click)="toggleDashboardEditMode()"
matTooltip="{{'dashboard.edit-mode' | translate}}" matTooltip="{{'dashboard.edit-mode' | translate}}"
matTooltipPosition="below"> matTooltipPosition="below">
<mat-icon>edit</mat-icon></button> <mat-icon>edit</mat-icon></button>
<button class="lt-lg:!hidden" mat-stroked-button (click)="toggleDashboardEditMode()"> <button class="lt-lg:!hidden" mat-stroked-button (click)="toggleDashboardEditMode()">
<mat-icon>edit</mat-icon>{{'dashboard.edit-mode' | translate}}</button> <mat-icon>edit</mat-icon>{{'dashboard.edit-mode' | translate}}</button>
</ng-container> }
<ng-container *ngIf="isEdit"> @if (isEdit) {
<button class="gt-md:!hidden" mat-icon-button (click)="saveDashboard()" [disabled]="isLoading$ | async" <button class="gt-md:!hidden" mat-icon-button (click)="saveDashboard()" [disabled]="isLoading$ | async"
matTooltip="{{'action.save' | translate}}" matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below"> matTooltipPosition="below">
<mat-icon>done</mat-icon></button> <mat-icon>done</mat-icon></button>
<button class="lt-lg:!hidden" mat-stroked-button [disabled]="isLoading$ | async" (click)="saveDashboard()"> <button class="lt-lg:!hidden" mat-stroked-button [disabled]="isLoading$ | async" (click)="saveDashboard()">
<mat-icon>done</mat-icon>{{ 'action.save' | translate }}</button> <mat-icon>done</mat-icon>{{ 'action.save' | translate }}</button>
<button class="gt-md:!hidden" mat-icon-button (click)="toggleDashboardEditMode()" <button class="gt-md:!hidden" mat-icon-button (click)="toggleDashboardEditMode()"
matTooltip="{{'action.cancel' | translate}}" matTooltip="{{'action.cancel' | translate}}"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
matTooltipPosition="below"> matTooltipPosition="below">
<mat-icon>close</mat-icon></button> <mat-icon>close</mat-icon></button>
<button class="lt-lg:!hidden" mat-button (click)="toggleDashboardEditMode()" [disabled]="isLoading$ | async"> <button class="lt-lg:!hidden" mat-button (click)="toggleDashboardEditMode()" [disabled]="isLoading$ | async">
<mat-icon>close</mat-icon>{{ 'action.cancel' | translate }}</button> <mat-icon>close</mat-icon>{{ 'action.cancel' | translate }}</button>
<mat-divider class="tb-toolbar-divider" vertical></mat-divider> <mat-divider class="tb-toolbar-divider" vertical></mat-divider>
</ng-container> }
<ng-container *ngIf="currentDashboardId && isEdit && isTenantAdmin()"> @if (currentDashboardId && isEdit && isTenantAdmin()) {
<button class="lt-lg:!hidden gt-lg:!hidden" mat-icon-button <button class="lt-lg:!hidden gt-lg:!hidden" mat-icon-button
#versionControlIconButton #versionControlIconButton
matTooltip="{{'version-control.version-control' | translate}}" matTooltip="{{'version-control.version-control' | translate}}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="toggleVersionControl($event, versionControlIconButton)"> (click)="toggleVersionControl($event, versionControlIconButton)">
<mat-icon>history</mat-icon> <mat-icon>history</mat-icon>
</button> </button>
<button class="lt-xl:!hidden" mat-button <button class="lt-xl:!hidden" mat-button
#versionControlButton #versionControlButton
matTooltip="{{'version-control.version-control' | translate}}" matTooltip="{{'version-control.version-control' | translate}}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="toggleVersionControl($event, versionControlButton)"> (click)="toggleVersionControl($event, versionControlButton)">
<mat-icon>history</mat-icon> <mat-icon>history</mat-icon>
{{'version-control.versions' | translate}} {{'version-control.versions' | translate}}
</button> </button>
</ng-container> }
<ng-container *ngIf="isEdit"> @if (isEdit) {
<button class="gt-lg:!hidden" mat-icon-button <button class="gt-lg:!hidden" mat-icon-button
matTooltip="{{ 'filter.filters' | translate }}" matTooltip="{{ 'filter.filters' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="openFilters($event)"> (click)="openFilters($event)">
<mat-icon>filter_list</mat-icon> <mat-icon>filter_list</mat-icon>
</button> </button>
<button class="lt-xl:!hidden" mat-button <button class="lt-xl:!hidden" mat-button
matTooltip="{{ 'filter.filters' | translate }}" matTooltip="{{ 'filter.filters' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="openFilters($event)"> (click)="openFilters($event)">
<mat-icon>filter_list</mat-icon> <mat-icon>filter_list</mat-icon>
{{ 'filter.filters' | translate }} {{ 'filter.filters' | translate }}
</button> </button>
</ng-container> }
<ng-container *ngIf="isEdit"> @if (isEdit) {
<button class="gt-lg:!hidden" mat-icon-button <button class="gt-lg:!hidden" mat-icon-button
matTooltip="{{ 'entity.aliases' | translate }}" matTooltip="{{ 'entity.aliases' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="openEntityAliases($event)"> (click)="openEntityAliases($event)">
<mat-icon>devices_other</mat-icon> <mat-icon>devices_other</mat-icon>
</button> </button>
<button class="lt-xl:!hidden" mat-button <button class="lt-xl:!hidden" mat-button
matTooltip="{{ 'entity.aliases' | translate }}" matTooltip="{{ 'entity.aliases' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="openEntityAliases($event)"> (click)="openEntityAliases($event)">
<mat-icon>devices_other</mat-icon> <mat-icon>devices_other</mat-icon>
{{ 'entity.aliases-short' | translate }} {{ 'entity.aliases-short' | translate }}
</button> </button>
</ng-container> }
<ng-container *ngIf="isEdit"> @if (isEdit) {
<button class="gt-lg:!hidden" mat-icon-button <button class="gt-lg:!hidden" mat-icon-button
matTooltip="{{ 'dashboard.settings' | translate }}" matTooltip="{{ 'dashboard.settings' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="openDashboardSettings($event)"> (click)="openDashboardSettings($event)">
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
</button> </button>
<button class="lt-xl:!hidden" mat-button <button class="lt-xl:!hidden" mat-button
matTooltip="{{ 'dashboard.settings' | translate }}" matTooltip="{{ 'dashboard.settings' | translate }}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="openDashboardSettings($event)"> (click)="openDashboardSettings($event)">
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
{{ 'dashboard.settings' | translate }} {{ 'dashboard.settings' | translate }}
</button> </button>
</ng-container> }
<button [class.!hidden]="!currentDashboardId || isEdit || isMobileApp || !isTenantAdmin() || !displayUpdateDashboardImage()" mat-icon-button <button [class.!hidden]="!currentDashboardId || isEdit || isMobileApp || !isTenantAdmin() || !displayUpdateDashboardImage()" mat-icon-button
matTooltip="{{'dashboard.update-image' | translate}}" matTooltip="{{'dashboard.update-image' | translate}}"
matTooltipPosition="below" matTooltipPosition="below"
(click)="updateDashboardImage($event)"> (click)="updateDashboardImage($event)">
<mat-icon>wallpaper</mat-icon> <mat-icon>wallpaper</mat-icon>
</button> </button>
<ng-container *ngIf="isEdit; else timewindowTemplate"> @if (isEdit) {
<tb-timewindow class="lt-xl:!hidden" asButton <tb-timewindow class="lt-xl:!hidden" asButton
flatButton flatButton
showSaveAsDefault showSaveAsDefault
[displayTimewindowValue]="false" [displayTimewindowValue]="false"
[isEdit]="true" [isEdit]="true"
tooltipPosition="below" tooltipPosition="below"
aggregation="true" aggregation="true"
timezone="true" timezone="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow"> [(ngModel)]="dashboardCtx.dashboardTimewindow">
</tb-timewindow> </tb-timewindow>
<tb-timewindow class="gt-lg:!hidden" <tb-timewindow class="gt-lg:!hidden"
hideLabel hideLabel
showSaveAsDefault showSaveAsDefault
[isEdit]="true" [isEdit]="true"
tooltipPosition="below" tooltipPosition="below"
aggregation="true" aggregation="true"
timezone="true" timezone="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow"> [(ngModel)]="dashboardCtx.dashboardTimewindow">
</tb-timewindow> </tb-timewindow>
</ng-container> } @else {
<ng-template #timewindowTemplate> @if (displayDashboardTimewindow()) {
<ng-container *ngIf="displayDashboardTimewindow()"> <tb-timewindow class="gt-md:!hidden"
<tb-timewindow class="gt-md:!hidden" [isEdit]="false"
[isEdit]="false" hideLabel
hideLabel tooltipPosition="below"
tooltipPosition="below" aggregation="true"
aggregation="true" timezone="true"
timezone="true" [showSaveAsDefault]="!readonly"
[showSaveAsDefault]="!readonly" [(ngModel)]="dashboardCtx.dashboardTimewindow"
[(ngModel)]="dashboardCtx.dashboardTimewindow" (saveAsDefault)="saveDashboard()">>
(saveAsDefault)="saveDashboard()">> </tb-timewindow>
</tb-timewindow> <tb-timewindow class="lt-lg:!hidden"
<tb-timewindow class="lt-lg:!hidden" [isEdit]="false"
[isEdit]="false" tooltipPosition="below"
tooltipPosition="below" aggregation="true"
aggregation="true" timezone="true"
timezone="true" [showSaveAsDefault]="!readonly"
[showSaveAsDefault]="!readonly" [(ngModel)]="dashboardCtx.dashboardTimewindow"
[(ngModel)]="dashboardCtx.dashboardTimewindow" (saveAsDefault)="saveDashboard()">>
(saveAsDefault)="saveDashboard()">> </tb-timewindow>
</tb-timewindow> }
</ng-container> }
</ng-template> @if (isEdit) {
<ng-container *ngIf="isEdit"> <button class="gt-md:!hidden" mat-icon-button
<button class="gt-md:!hidden" mat-icon-button matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}" matTooltipPosition="below"
matTooltipPosition="below" (click)="addWidget($event)">
(click)="addWidget($event)"> <mat-icon>add</mat-icon>
<mat-icon>add</mat-icon> </button>
</button> <button class="lt-lg:!hidden" mat-stroked-button
<button class="lt-lg:!hidden" mat-stroked-button matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}" matTooltipPosition="below"
matTooltipPosition="below" (click)="addWidget($event)">
(click)="addWidget($event)"> <mat-icon>add</mat-icon>
<mat-icon>add</mat-icon> {{ 'dashboard.add-widget-button-text' | translate }}
{{ 'dashboard.add-widget-button-text' | translate }} </button>
</button> }
</ng-container> <tb-filters-edit [class.!hidden]="isEdit || !displayFilters()"
<tb-filters-edit [class.!hidden]="isEdit || !displayFilters()" tooltipPosition="below"
tooltipPosition="below" [aliasController]="dashboardCtx.aliasController">
[aliasController]="dashboardCtx.aliasController"> </tb-filters-edit>
</tb-filters-edit> <tb-aliases-entity-select [class.!hidden]="isEdit || !displayEntitiesSelect()"
<tb-aliases-entity-select [class.!hidden]="isEdit || !displayEntitiesSelect()" tooltipPosition="below"
tooltipPosition="below" [aliasController]="dashboardCtx.aliasController">
[aliasController]="dashboardCtx.aliasController"> </tb-aliases-entity-select>
</tb-aliases-entity-select> @if (!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()) {
<tb-dashboard-select *ngIf="!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()" <tb-dashboard-select
[(ngModel)]="currentDashboardId" [(ngModel)]="currentDashboardId"
(ngModelChange)="currentDashboardIdChanged(currentDashboardId)" (ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
[customerId]="currentCustomerId" [customerId]="currentCustomerId"
[dashboardsScope]="currentDashboardScope"> [dashboardsScope]="currentDashboardScope">
</tb-dashboard-select> </tb-dashboard-select>
</div> }
</div> </div>
</tb-dashboard-toolbar> </div>
</section> </tb-dashboard-toolbar>
<section class="tb-dashboard-container tb-absolute-fill" </section>
tb-toast toastTarget="dashboardRoot" <section class="tb-dashboard-container tb-absolute-fill"
#dashboardContainer tb-toast toastTarget="dashboardRoot"
[class.is-fullscreen]="forceFullscreen" #dashboardContainer
[class.tb-dashboard-toolbar-opened]="toolbarOpened" [class.is-fullscreen]="forceFullscreen"
[class.tb-dashboard-toolbar-animated]="isToolbarOpenedAnimate" [class.tb-dashboard-toolbar-opened]="toolbarOpened"
[class.tb-dashboard-toolbar-closed]="!toolbarOpened"> [class.tb-dashboard-toolbar-animated]="isToolbarOpenedAnimate"
<mat-drawer-container hasBackdrop="false" class="tb-absolute-fill tb-dashboard-drawer-container"> [class.tb-dashboard-toolbar-closed]="!toolbarOpened">
<mat-drawer-content #dashboardContent class="flex flex-col items-start justify-center"> <mat-drawer-container hasBackdrop="false" class="tb-absolute-fill tb-dashboard-drawer-container">
<section *ngIf="!widgetEditMode" class="tb-dashboard-title" <mat-drawer-content #dashboardContent class="flex flex-col items-start justify-center">
[style.color]="dashboard.configuration.settings.titleColor"> @if (!widgetEditMode) {
<h3 [class.!hidden]="isEdit || !displayTitle()">{{ translatedDashboardTitle }}</h3> <section class="tb-dashboard-title"
<mat-form-field [class.!hidden]="!isEdit" class="mat-block tb-appearance-transparent" subscriptSizing="dynamic"> [style.color]="dashboard.configuration.settings.titleColor">
<mat-label translate [style.color]="dashboard.configuration.settings.titleColor">dashboard.title</mat-label> <h3 [class.!hidden]="isEdit || !displayTitle()">{{ translatedDashboardTitle }}</h3>
<input matInput class="tb-dashboard-title" <mat-form-field [class.!hidden]="!isEdit" class="mat-block tb-appearance-transparent" subscriptSizing="dynamic">
[style.color]="dashboard.configuration.settings.titleColor" <mat-label translate [style.color]="dashboard.configuration.settings.titleColor">dashboard.title</mat-label>
required name="title" [(ngModel)]="dashboard.title"> <input matInput class="tb-dashboard-title"
</mat-form-field> [style.color]="dashboard.configuration.settings.titleColor"
</section> required name="title" [(ngModel)]="dashboard.title">
<mat-drawer-container class="tb-dashboard-layouts flex-1" </mat-form-field>
[class.tb-shrinked]="isEditingWidget"> </section>
<mat-drawer *ngIf="layouts.right.show" }
id="tb-right-layout" <mat-drawer-container class="tb-dashboard-layouts flex-1"
style="border-left: none" [class.tb-shrinked]="isEditingWidget">
[style.min-width]="rightLayoutSize.width" @if (layouts.right.show) {
[style.max-width]="rightLayoutSize.width" <mat-drawer
[style.height]="rightLayoutSize.height" id="tb-right-layout"
disableClose="true" style="border-left: none"
[@.disabled]="!isMobile" [style.min-width]="rightLayoutSize.width"
position="end" [style.max-width]="rightLayoutSize.width"
[mode]="isMobile ? 'over' : 'side'" [style.height]="rightLayoutSize.height"
[(opened)]="rightLayoutOpened"> disableClose="true"
<tb-dashboard-layout style="height: 100%;" [@.disabled]="!isMobile"
[dashboardCheatSheet]="cheatSheetComponent" position="end"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.main.layoutCtx : layouts.right.layoutCtx" [mode]="isMobile ? 'over' : 'side'"
[dashboardCtx]="dashboardCtx" [(opened)]="rightLayoutOpened">
[isEdit]="isEdit" <tb-dashboard-layout style="height: 100%;"
[isEditingWidget]="isEditingWidget" [dashboardCheatSheet]="cheatSheetComponent"
[isMobile]="forceDashboardMobileMode" [layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.main.layoutCtx : layouts.right.layoutCtx"
[widgetEditMode]="widgetEditMode" [dashboardCtx]="dashboardCtx"
[parentDashboard]="parentDashboard" [isEdit]="isEdit"
[popoverComponent]="popoverComponent"> [isEditingWidget]="isEditingWidget"
</tb-dashboard-layout> [isMobile]="forceDashboardMobileMode"
</mat-drawer> [widgetEditMode]="widgetEditMode"
<mat-drawer-content [class.!hidden]="!layouts.main.show" [parentDashboard]="parentDashboard"
id="tb-main-layout" [popoverComponent]="popoverComponent">
[style.width]="mainLayoutSize.width" </tb-dashboard-layout>
[style.height]="mainLayoutSize.height"> </mat-drawer>
<tb-dashboard-layout }
[style.max-width]="mainLayoutSize.maxWidth" <mat-drawer-content [class.!hidden]="!layouts.main.show"
[style.min-width]="mainLayoutSize.minWidth" id="tb-main-layout"
[dashboardCheatSheet]="cheatSheetComponent" [style.width]="mainLayoutSize.width"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx" [style.height]="mainLayoutSize.height">
[dashboardCtx]="dashboardCtx" <tb-dashboard-layout
[isEdit]="isEdit" [style.max-width]="mainLayoutSize.maxWidth"
[isEditingWidget]="isEditingWidget" [style.min-width]="mainLayoutSize.minWidth"
[isMobile]="forceDashboardMobileMode" [dashboardCheatSheet]="cheatSheetComponent"
[widgetEditMode]="widgetEditMode" [layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[parentDashboard]="parentDashboard" [dashboardCtx]="dashboardCtx"
[popoverComponent]="popoverComponent"> [isEdit]="isEdit"
</tb-dashboard-layout> [isEditingWidget]="isEditingWidget"
<div class="layout-filler" [isMobile]="forceDashboardMobileMode"
*ngIf="isEdit"
[class.!hidden]="!showMainLayoutFiller">
{{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }}
</div>
</mat-drawer-content>
</mat-drawer-container>
<section data-html2canvas-ignore class="tb-header-buttons tb-enter-edit-mode flex flex-row flex-wrap items-end justify-start" *ngIf="!readonly && (hideToolbar || widgetEditMode)">
<button [class.!hidden]="isEdit"
color="primary"
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="toggleDashboardEditMode()"
matTooltip="{{ 'action.enter-edit-mode' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [class.!hidden]="!isEdit"
color="primary"
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="saveDashboard()"
[disabled]="isLoading$ | async"
matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below">
<mat-icon>done</mat-icon></button>
<button [class.!hidden]="!isEdit"
color="primary"
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="toggleDashboardEditMode()"
[disabled]="isLoading$ | async"
matTooltip="{{'action.cancel' | translate}}"
matTooltipPosition="below">
<mat-icon>close</mat-icon></button>
</section>
<section *ngIf="!embedded" data-html2canvas-ignore class="tb-powered-by-footer" [style.color]="dashboard.configuration.settings.titleColor">
<span>Powered by <a href="https://thingsboard.io" target="_blank">ThingsBoard v.{{ thingsboardVersion }}</a></span>
</section>
</mat-drawer-content>
<mat-drawer class="tb-details-drawer"
[opened]="isEditingWidget || isAddingWidget"
(openedStart)="detailsDrawerOpenedStart()"
(closed)="detailsDrawerClosed()"
disableClose
mode="over"
position="end">
<tb-details-panel *ngIf="!isEditingWidgetClosed" class="flex-1"
headerTitle="{{editingWidget?.config.title}}"
headerSubtitle="{{ editingWidgetSubtitle }}"
[isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()">
<div class="details-buttons flex flex-row items-center justify-start gap-4">
<tb-toggle-select *ngIf="tbEditWidget.widgetConfig?.hasBasicMode" appearance="fill-invert" selectMediaBreakpoint="xs" [(ngModel)]="tbEditWidget.widgetConfigMode">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<tb-edit-widget #tbEditWidget
[dashboard]="dashboard"
[aliasController]="dashboardCtx.aliasController"
[stateController]="dashboardCtx.stateController"
[widgetEditMode]="widgetEditMode" [widgetEditMode]="widgetEditMode"
[widget]="editingWidget" [parentDashboard]="parentDashboard"
[widgetLayout]="editingWidgetLayout" [popoverComponent]="popoverComponent">
[showLayoutConfig]="showLayoutConfigInEdit(editingLayoutCtx)" </tb-dashboard-layout>
[isDefaultBreakpoint]="editingLayoutCtx?.breakpoint === 'default'" @if (isEdit) {
(revertWidgetConfig)="onRevertWidgetEdit()" <div class="layout-filler"
(applyWidgetConfig)="saveWidget()"> [class.!hidden]="!showMainLayoutFiller">
</tb-edit-widget> {{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }}
</tb-details-panel> </div>
<tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" class="flex-1" }
</mat-drawer-content>
</mat-drawer-container>
@if (!readonly && (hideToolbar || widgetEditMode)) {
<section data-html2canvas-ignore class="tb-header-buttons tb-enter-edit-mode flex flex-row flex-wrap items-end justify-start">
<button [class.!hidden]="isEdit"
color="primary"
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="toggleDashboardEditMode()"
matTooltip="{{ 'action.enter-edit-mode' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [class.!hidden]="!isEdit"
color="primary"
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="saveDashboard()"
[disabled]="isLoading$ | async"
matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below">
<mat-icon>done</mat-icon></button>
<button [class.!hidden]="!isEdit"
color="primary"
mat-mini-fab
class="tb-btn-header tb-btn-edit"
(click)="toggleDashboardEditMode()"
[disabled]="isLoading$ | async"
matTooltip="{{'action.cancel' | translate}}"
matTooltipPosition="below">
<mat-icon>close</mat-icon></button>
</section>
}
@if (!embedded) {
<section data-html2canvas-ignore class="tb-powered-by-footer" [style.color]="dashboard.configuration.settings.titleColor">
<span>Powered by <a href="https://thingsboard.io" target="_blank">ThingsBoard v.{{ thingsboardVersion }}</a></span>
</section>
}
</mat-drawer-content>
<mat-drawer class="tb-details-drawer"
[opened]="isEditingWidget || isAddingWidget"
(openedStart)="detailsDrawerOpenedStart()"
(closed)="detailsDrawerClosed()"
disableClose
mode="over"
position="end">
@if (!isEditingWidgetClosed) {
<tb-details-panel class="flex-1"
headerTitle="{{editingWidget?.config.title}}"
headerSubtitle="{{ editingWidgetSubtitle }}"
[isReadOnly]="true"
(closeDetails)="onEditWidgetClosed()">
<div class="details-buttons flex flex-row items-center justify-start gap-4">
@if (tbEditWidget.widgetConfig?.hasBasicMode) {
<tb-toggle-select appearance="fill-invert" selectMediaBreakpoint="xs" [(ngModel)]="tbEditWidget.widgetConfigMode">
<tb-toggle-option value="basic">{{ 'widget.basic-mode' | translate }}</tb-toggle-option>
<tb-toggle-option value="advanced">{{ 'widget.advanced-mode' | translate }}</tb-toggle-option>
</tb-toggle-select>
}
<div [tb-help]="helpLinkIdForWidgetType()"></div>
</div>
<tb-edit-widget #tbEditWidget
[dashboard]="dashboard"
[aliasController]="dashboardCtx.aliasController"
[stateController]="dashboardCtx.stateController"
[widgetEditMode]="widgetEditMode"
[widget]="editingWidget"
[widgetLayout]="editingWidgetLayout"
[showLayoutConfig]="showLayoutConfigInEdit(editingLayoutCtx)"
[isDefaultBreakpoint]="editingLayoutCtx?.breakpoint === 'default'"
(revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()">
</tb-edit-widget>
</tb-details-panel>
}
@if (!isAddingWidgetClosed && !widgetEditMode) {
<tb-details-panel class="flex-1"
headerTitle="{{ isAddingWidget ? headerTitle="{{ isAddingWidget ?
((dashboardWidgetSelectComponent?.selectWidgetMode === 'allWidgets' ? ('dashboard.select-widget-value' | translate: { title: ('widget.all-widgets' | translate) }) : ((dashboardWidgetSelectComponent?.selectWidgetMode === 'allWidgets' ? ('dashboard.select-widget-value' | translate: { title: ('widget.all-widgets' | translate) }) :
(!dashboardWidgetSelectComponent?.widgetsBundle ? (!dashboardWidgetSelectComponent?.widgetsBundle ?
'widget.select-widgets-bundle' : 'dashboard.select-widget-value') | translate: dashboardWidgetSelectComponent?.widgetsBundle)) : '' 'widget.select-widgets-bundle' : 'dashboard.select-widget-value') | translate: dashboardWidgetSelectComponent?.widgetsBundle)) : ''
}}" }}"
headerHeightPx="64" headerHeightPx="64"
[isShowSearch]="true" [isShowSearch]="true"
[isReadOnly]="true" [isReadOnly]="true"
[isEdit]="false" [isEdit]="false"
backgroundColor="#cfd8dc" backgroundColor="#cfd8dc"
(closeDetails)="onAddWidgetClosed()" (closeDetails)="onAddWidgetClosed()"
(closeSearch)="dashboardWidgetSelectComponent.search = ''"> (closeSearch)="dashboardWidgetSelectComponent.search = ''">
<div class="prefix-title-buttons" [class.!hidden]="!(isAddingWidget && (dashboardWidgetSelectComponent?.widgetsBundle || dashboardWidgetSelectComponent?.selectWidgetMode === 'allWidgets'))" style="height: 28px; margin-right: 12px"> <div class="prefix-title-buttons" [class.!hidden]="!(isAddingWidget && (dashboardWidgetSelectComponent?.widgetsBundle || dashboardWidgetSelectComponent?.selectWidgetMode === 'allWidgets'))" style="height: 28px; margin-right: 12px">
<button class="tb-mat-28" mat-icon-button type="button" (click)="clearSelectedWidgetBundle()"> <button class="tb-mat-28" mat-icon-button type="button" (click)="clearSelectedWidgetBundle()">
<mat-icon>arrow_back</mat-icon> <mat-icon>arrow_back</mat-icon>
</button> </button>
</div> </div>
<div class="search-pane flex flex-row" *ngIf="isAddingWidget && dashboardWidgetSelectComponent"> @if (isAddingWidget && dashboardWidgetSelectComponent) {
<tb-widgets-bundle-search class="flex-1" <div class="search-pane flex flex-row">
[(ngModel)]="dashboardWidgetSelectComponent.search" <tb-widgets-bundle-search class="flex-1"
[(ngModel)]="dashboardWidgetSelectComponent.search"
placeholder="{{ ((!dashboardWidgetSelectComponent.widgetsBundle && dashboardWidgetSelectComponent.selectWidgetMode === 'bundles') placeholder="{{ ((!dashboardWidgetSelectComponent.widgetsBundle && dashboardWidgetSelectComponent.selectWidgetMode === 'bundles')
? 'widgets-bundle.search' : 'widget.search') | translate }}"> ? 'widgets-bundle.search' : 'widget.search') | translate }}">
</tb-widgets-bundle-search> </tb-widgets-bundle-search>
</div> </div>
<div class="details-buttons flex flex-row items-center justify-start" *ngIf="isAddingWidget"> }
<button mat-button type="button" (click)="importWidget($event)" @if (isAddingWidget) {
*ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle"> <div class="details-buttons flex flex-row items-center justify-start">
<mat-icon>file_upload</mat-icon>{{ 'dashboard.import-widget' | translate }}</button> @if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) {
<tb-toggle-select *ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle" <button mat-button type="button" (click)="importWidget($event)"
appearance="fill-invert" >
disablePagination <mat-icon>file_upload</mat-icon>{{ 'dashboard.import-widget' | translate }}</button>
selectMediaBreakpoint="xs" }
[(ngModel)]="dashboardWidgetSelectComponent.selectWidgetMode"> @if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) {
<tb-toggle-option value="bundles">{{ 'widgets-bundle.widgets-bundles' | translate }}</tb-toggle-option> <tb-toggle-select
<tb-toggle-option value="allWidgets">{{ 'widget.all-widgets' | translate }}</tb-toggle-option> appearance="fill-invert"
</tb-toggle-select> disablePagination
<button mat-icon-button type="button" selectMediaBreakpoint="xs"
*ngIf="dashboardWidgetSelectComponent?.widgetTypes.size > 1" [(ngModel)]="dashboardWidgetSelectComponent.selectWidgetMode">
(click)="editWidgetsTypesToDisplay($event)" <tb-toggle-option value="bundles">{{ 'widgets-bundle.widgets-bundles' | translate }}</tb-toggle-option>
matTooltip="{{ 'widget.filter' | translate }}" <tb-toggle-option value="allWidgets">{{ 'widget.all-widgets' | translate }}</tb-toggle-option>
matTooltipPosition="above"> </tb-toggle-select>
<mat-icon>filter_list</mat-icon> }
</button> @if (dashboardWidgetSelectComponent?.widgetTypes.size > 1) {
<tb-toggle-select *ngIf="dashboardWidgetSelectComponent?.hasDeprecated" <button mat-icon-button type="button"
appearance="fill-invert" (click)="editWidgetsTypesToDisplay($event)"
disablePagination matTooltip="{{ 'widget.filter' | translate }}"
selectMediaBreakpoint="xs" matTooltipPosition="above">
[(ngModel)]="dashboardWidgetSelectComponent.deprecatedFilter"> <mat-icon>filter_list</mat-icon>
<tb-toggle-option value="ALL">{{ 'widget.all' | translate }}</tb-toggle-option> </button>
<tb-toggle-option value="ACTUAL">{{ 'widget.actual' | translate }}</tb-toggle-option> }
<tb-toggle-option value="DEPRECATED">{{ 'widget.deprecated' | translate }}</tb-toggle-option> @if (dashboardWidgetSelectComponent?.hasDeprecated) {
</tb-toggle-select> <tb-toggle-select
</div> appearance="fill-invert"
<tb-dashboard-widget-select #dashboardWidgetSelect disablePagination
*ngIf="isAddingWidget" selectMediaBreakpoint="xs"
[aliasController]="dashboardCtx.aliasController" [(ngModel)]="dashboardWidgetSelectComponent.deprecatedFilter">
[filterWidgetTypes]="filterWidgetTypes" <tb-toggle-option value="ALL">{{ 'widget.all' | translate }}</tb-toggle-option>
[scadaFirst]="layouts.main.layoutCtx.gridSettings.layoutType === LayoutType.scada" <tb-toggle-option value="ACTUAL">{{ 'widget.actual' | translate }}</tb-toggle-option>
(widgetSelected)="addWidgetFromType($event)"> <tb-toggle-option value="DEPRECATED">{{ 'widget.deprecated' | translate }}</tb-toggle-option>
</tb-dashboard-widget-select> </tb-toggle-select>
</tb-details-panel> }
</mat-drawer> </div>
</mat-drawer-container> }
</section> @if (isAddingWidget) {
</div> <tb-dashboard-widget-select #dashboardWidgetSelect
[aliasController]="dashboardCtx.aliasController"
[filterWidgetTypes]="filterWidgetTypes"
[scadaFirst]="layouts.main.layoutCtx.gridSettings.layoutType === LayoutType.scada"
(widgetSelected)="addWidgetFromType($event)">
</tb-dashboard-widget-select>
}
</tb-details-panel>
}
</mat-drawer>
</mat-drawer-container>
</section>
</div>

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

@ -25,224 +25,258 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content class="tb-form-panel no-border no-padding"> <div mat-dialog-content class="tb-form-panel no-border no-padding">
<div *ngIf="settings" class="tb-form-panel no-border no-padding" [formGroup]="settingsFormGroup"> @if (settings) {
<div class="tb-form-row space-between"> <div class="tb-form-panel no-border no-padding" [formGroup]="settingsFormGroup">
<div>{{ 'dashboard.state-controller' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="stateControllerId">
<mat-option *ngFor="let stateControllerId of stateControllerIds" [value]="stateControllerId">
{{ getStatesControllerTranslation(stateControllerId) }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.title-settings</div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitle"> <div>{{ 'dashboard.state-controller' | translate }}</div>
{{ 'dashboard.title' | translate }}
</mat-slide-toggle>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.dashboard-logo-settings</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="showDashboardLogo">
{{ 'dashboard.display-dashboard-logo' | translate }}
</mat-slide-toggle>
</div>
<tb-gallery-image-input class="flex-1"
label="{{'dashboard.dashboard-logo-image' | translate}}"
formControlName="dashboardLogoUrl">
</tb-gallery-image-input>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.toolbar-settings</div>
<mat-slide-toggle class="mat-slide" formControlName="hideToolbar">
{{ 'dashboard.hide-toolbar' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="toolbarAlwaysOpen">
{{ 'dashboard.toolbar-always-open' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showDashboardsSelect">
{{ 'dashboard.display-dashboards-selection' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showEntitiesSelect">
{{ 'dashboard.display-entities-selection' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showFilters">
{{ 'dashboard.display-filters' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showDashboardTimewindow">
{{ 'dashboard.display-dashboard-timewindow' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showDashboardExport">
{{ 'dashboard.display-dashboard-export' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showUpdateDashboardImage">
{{ 'dashboard.display-update-dashboard-image' | translate }}
</mat-slide-toggle>
</div>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description class="flex items-stretch justify-end" translate>
dashboard.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-css
label="{{ 'dashboard.dashboard-css' | translate }}"
formControlName="dashboardCss"
></tb-css>
</ng-template>
</mat-expansion-panel>
</div>
<div *ngIf="gridSettings" class="tb-form-panel no-border no-padding" [formGroup]="gridSettingsFormGroup">
<div class="tb-form-panel stroked">
<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-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="viewFormat"> <mat-select required formControlName="stateControllerId">
<mat-option *ngFor="let type of viewFormatTypes" [value]="type"> @for (stateControllerId of stateControllerIds; track stateControllerId) {
{{ viewFormatTypeTranslationMap.get(type) | translate }} <mat-option [value]="stateControllerId">
</mat-option> {{ getStatesControllerTranslation(stateControllerId) }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-panel stroked">
<div translate>dashboard.columns-count</div> <div class="tb-form-panel-title" translate>dashboard.title-settings</div>
<mat-form-field *ngIf="!isScada()" appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic" <div class="tb-form-row space-between">
matTooltipPosition="above" <mat-slide-toggle class="mat-slide" formControlName="showTitle">
matTooltipClass="tb-error-tooltip" {{ 'dashboard.title' | translate }}
[matTooltipDisabled]="!(gridSettingsFormGroup.get('columns').invalid && gridSettingsFormGroup.get('columns').touched)" </mat-slide-toggle>
<tb-color-input asBoxInput
colorClearButton
formControlName="titleColor">
</tb-color-input>
</div>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.dashboard-logo-settings</div>
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="showDashboardLogo">
{{ 'dashboard.display-dashboard-logo' | translate }}
</mat-slide-toggle>
</div>
<tb-gallery-image-input class="flex-1"
label="{{'dashboard.dashboard-logo-image' | translate}}"
formControlName="dashboardLogoUrl">
</tb-gallery-image-input>
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.toolbar-settings</div>
<mat-slide-toggle class="mat-slide" formControlName="hideToolbar">
{{ 'dashboard.hide-toolbar' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="toolbarAlwaysOpen">
{{ 'dashboard.toolbar-always-open' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showDashboardsSelect">
{{ 'dashboard.display-dashboards-selection' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showEntitiesSelect">
{{ 'dashboard.display-entities-selection' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showFilters">
{{ 'dashboard.display-filters' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showDashboardTimewindow">
{{ 'dashboard.display-dashboard-timewindow' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showDashboardExport">
{{ 'dashboard.display-dashboard-export' | translate }}
</mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="showUpdateDashboardImage">
{{ 'dashboard.display-update-dashboard-image' | translate }}
</mat-slide-toggle>
</div>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description class="flex items-stretch justify-end" translate>
dashboard.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-css
label="{{ 'dashboard.dashboard-css' | translate }}"
formControlName="dashboardCss"
></tb-css>
</ng-template>
</mat-expansion-panel>
</div>
}
@if (gridSettings) {
<div class="tb-form-panel no-border no-padding" [formGroup]="gridSettingsFormGroup">
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate [translateParams]="{type: layoutSettingsType}">dashboard.layout-settings-type</div>
@if (isDefaultLayout && !isDefaultBreakpoint) {
<div 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">
@for (type of viewFormatTypes; track type) {
<mat-option [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>
@if (!isScada()) {
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('columns').invalid && gridSettingsFormGroup.get('columns').touched)"
[matTooltip]="(gridSettingsFormGroup.get('columns').hasError('required') ? 'dashboard.columns-count-required' : [matTooltip]="(gridSettingsFormGroup.get('columns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('columns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"> (gridSettingsFormGroup.get('columns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate">
<input matInput formControlName="columns" type="number" min="10" max="1008" step="1" <input matInput formControlName="columns" type="number" min="10" max="1008" step="1"
required required
placeholder="{{ 'widget-config.set' | translate }}"> placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
<mat-form-field *ngIf="isScada()" }
class="medium-width" appearance="outline" subscriptSizing="dynamic"> @if (isScada()) {
<mat-select required formControlName="columns"> <mat-form-field
<mat-option *ngFor="let columns of scadaColumns" [value]="columns"> class="medium-width" appearance="outline" subscriptSizing="dynamic">
{{ columns }} <mat-select required formControlName="columns">
</mat-option> @for (columns of scadaColumns; track columns) {
</mat-select> <mat-option [value]="columns">
</mat-form-field> {{ columns }}
</div> </mat-option>
<div class="tb-form-row space-between"> }
<div translate>dashboard.min-layout-width</div> </mat-select>
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic" </mat-form-field>
matTooltipPosition="above" }
matTooltipClass="tb-error-tooltip" </div>
[matTooltipDisabled]="!(gridSettingsFormGroup.get('minColumns').invalid && gridSettingsFormGroup.get('minColumns').touched)" <div class="tb-form-row space-between">
<div translate>dashboard.min-layout-width</div>
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('minColumns').invalid && gridSettingsFormGroup.get('minColumns').touched)"
[matTooltip]="(gridSettingsFormGroup.get('minColumns').hasError('required') ? 'dashboard.columns-count-required' : [matTooltip]="(gridSettingsFormGroup.get('minColumns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('minColumns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"> (gridSettingsFormGroup.get('minColumns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate">
<input matInput formControlName="minColumns" type="number" min="10" max="1008" step="1" <input matInput formControlName="minColumns" type="number" min="10" max="1008" step="1"
required required
placeholder="{{ 'widget-config.set' | translate }}"> placeholder="{{ 'widget-config.set' | translate }}">
<span translate matSuffix>dashboard.columns-suffix</span> <span translate matSuffix>dashboard.columns-suffix</span>
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="!isScada()" class="tb-form-row space-between"> @if (!isScada()) {
<div translate>dashboard.widgets-margins</div> <div class="tb-form-row space-between">
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic" <div translate>dashboard.widgets-margins</div>
matTooltipPosition="above" <mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipClass="tb-error-tooltip" matTooltipPosition="above"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched)" matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched)"
[matTooltip]="(gridSettingsFormGroup.get('margin').hasError('required') ? 'dashboard.margin-required' : [matTooltip]="(gridSettingsFormGroup.get('margin').hasError('required') ? 'dashboard.margin-required' :
(gridSettingsFormGroup.get('margin').hasError('min') ? 'dashboard.min-margin-message' : 'dashboard.max-margin-message')) | translate"> (gridSettingsFormGroup.get('margin').hasError('min') ? 'dashboard.min-margin-message' : 'dashboard.max-margin-message')) | translate">
<input matInput formControlName="margin" type="number" min="0" max="50" step="any" <input matInput formControlName="margin" type="number" min="0" max="50" step="any"
required required
placeholder="{{ 'widget-config.set' | translate }}"> placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="!isScada()" class="tb-form-row"> }
<mat-slide-toggle class="mat-slide" formControlName="outerMargin"> @if (!isScada()) {
{{ 'dashboard.apply-outer-margin' | translate }} <div class="tb-form-row">
</mat-slide-toggle> <mat-slide-toggle class="mat-slide" formControlName="outerMargin">
</div> {{ 'dashboard.apply-outer-margin' | translate }}
<div *ngIf="!isScada()" class="tb-form-row"> </mat-slide-toggle>
<mat-slide-toggle class="mat-slide" formControlName="autoFillHeight"> </div>
{{ 'dashboard.autofill-height' | translate }} }
</mat-slide-toggle> @if (!isScada()) {
</div> <div class="tb-form-row">
<div *ngIf="isDefaultLayout && !isDefaultBreakpoint" class="tb-form-row space-between"> <mat-slide-toggle class="mat-slide" formControlName="autoFillHeight">
<div translate>dashboard.row-height</div> {{ 'dashboard.autofill-height' | translate }}
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic" </mat-slide-toggle>
matTooltipPosition="above" </div>
matTooltipClass="tb-error-tooltip" }
[matTooltipDisabled]="!(gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched)" @if (isDefaultLayout && !isDefaultBreakpoint) {
<div class="tb-form-row space-between">
<div translate>dashboard.row-height</div>
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched)"
[matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' : [matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ? (gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate"> 'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate">
<input matInput formControlName="rowHeight" type="number" min="5" max="200" step="any" <input matInput formControlName="rowHeight" type="number" min="5" max="200" step="any"
required required
placeholder="{{ 'widget-config.set' | translate }}"> placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span> <span matSuffix>px</span>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row space-between"> }
<div translate>dashboard.background-color</div> <div class="tb-form-row space-between">
<tb-color-input asBoxInput <div translate>dashboard.background-color</div>
colorClearButton <tb-color-input asBoxInput
formControlName="backgroundColor"> colorClearButton
</tb-color-input> formControlName="backgroundColor">
</tb-color-input>
</div>
<tb-gallery-image-input class="flex-1"
label="{{'dashboard.background-image' | translate}}"
formControlName="backgroundImageUrl">
</tb-gallery-image-input>
<div class="tb-form-row space-between">
<div>{{ 'dashboard.background-size-mode' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="backgroundSizeMode">
<mat-option value="100%">Fit width</mat-option>
<mat-option value="auto 100%">Fit height</mat-option>
<mat-option value="cover">Cover</mat-option>
<mat-option value="contain">Contain</mat-option>
<mat-option value="auto">Original size</mat-option>
</mat-select>
</mat-form-field>
</div>
</div> </div>
<tb-gallery-image-input class="flex-1" @if (!isScada() && isDefaultBreakpoint) {
label="{{'dashboard.background-image' | translate}}" <div class="tb-form-panel stroked">
formControlName="backgroundImageUrl"> <div class="tb-form-panel-title" translate>dashboard.mobile-layout</div>
</tb-gallery-image-input> @if (isRightLayout) {
<div class="tb-form-row space-between"> <div class="tb-form-row">
<div>{{ 'dashboard.background-size-mode' | translate }}</div> <mat-slide-toggle class="mat-slide" formControlName="mobileDisplayLayoutFirst">
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic"> {{ 'dashboard.display-first-in-mobile-view' | translate }}
<mat-select formControlName="backgroundSizeMode"> </mat-slide-toggle>
<mat-option value="100%">Fit width</mat-option> </div>
<mat-option value="auto 100%">Fit height</mat-option> }
<mat-option value="cover">Cover</mat-option> @if (!isScada()) {
<mat-option value="contain">Contain</mat-option> <div class="tb-form-row">
<mat-option value="auto">Original size</mat-option> <mat-slide-toggle class="mat-slide" formControlName="mobileAutoFillHeight">
</mat-select> {{ 'dashboard.autofill-height' | translate }}
</mat-form-field> </mat-slide-toggle>
</div> </div>
</div> }
<div *ngIf="!isScada() && isDefaultBreakpoint" class="tb-form-panel stroked"> @if (!isScada()) {
<div class="tb-form-panel-title" translate>dashboard.mobile-layout</div> <div class="tb-form-row space-between">
<div *ngIf="isRightLayout" class="tb-form-row"> <div translate>dashboard.mobile-row-height</div>
<mat-slide-toggle class="mat-slide" formControlName="mobileDisplayLayoutFirst"> <mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
{{ 'dashboard.display-first-in-mobile-view' | translate }} matTooltipPosition="above"
</mat-slide-toggle> matTooltipClass="tb-error-tooltip"
</div> [matTooltipDisabled]="!(gridSettingsFormGroup.get('mobileRowHeight').invalid && gridSettingsFormGroup.get('mobileRowHeight').touched)"
<div *ngIf="!isScada()" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="mobileAutoFillHeight">
{{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="!isScada()" class="tb-form-row space-between">
<div translate>dashboard.mobile-row-height</div>
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('mobileRowHeight').invalid && gridSettingsFormGroup.get('mobileRowHeight').touched)"
[matTooltip]="(gridSettingsFormGroup.get('mobileRowHeight').hasError('required') ? 'dashboard.mobile-row-height-required' : [matTooltip]="(gridSettingsFormGroup.get('mobileRowHeight').hasError('required') ? 'dashboard.mobile-row-height-required' :
(gridSettingsFormGroup.get('mobileRowHeight').hasError('min') ? (gridSettingsFormGroup.get('mobileRowHeight').hasError('min') ?
'dashboard.min-mobile-row-height-message' : 'dashboard.max-mobile-row-height-message')) | translate"> 'dashboard.min-mobile-row-height-message' : 'dashboard.max-mobile-row-height-message')) | translate">
<input matInput formControlName="mobileRowHeight" type="number" min="5" max="200" step="any" <input matInput formControlName="mobileRowHeight" type="number" min="5" max="200" step="any"
required required
placeholder="{{ 'widget-config.set' | translate }}"> placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span> <span matSuffix>px</span>
</mat-form-field> </mat-form-field>
</div> </div>
}
</div>
}
</div> </div>
</div> }
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button color="primary" <button mat-button color="primary"

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

@ -15,15 +15,19 @@
limitations under the License. limitations under the License.
--> -->
<tb-dashboard-page *ngIf="stateExists" @if (stateExists) {
[embedded]="true" <tb-dashboard-page
[syncStateWithQueryParam]="false" [embedded]="true"
[hideToolbar]="true" [syncStateWithQueryParam]="false"
[currentState]="currentState" [hideToolbar]="true"
[dashboard]="dashboard" [currentState]="currentState"
[parentDashboard]="parentDashboard" [dashboard]="dashboard"
[parentAliasController]="parentAliasController"> [parentDashboard]="parentDashboard"
</tb-dashboard-page> [parentAliasController]="parentAliasController">
<div class="tb-absolute-fill tb-widget-error" *ngIf="!stateExists"> </tb-dashboard-page>
<span>{{ 'dashboard.non-existent-dashboard-state-error' | translate:{stateId} }}</span> }
</div> @if (!stateExists) {
<div class="tb-absolute-fill tb-widget-error">
<span>{{ 'dashboard.non-existent-dashboard-state-error' | translate:{stateId} }}</span>
</div>
}

100
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html

@ -16,14 +16,55 @@
--> -->
<div class="tb-dashboard-widget-select"> <div class="tb-dashboard-widget-select">
<ng-container *ngIf="selectWidgetMode === 'bundles'; else allWidgets"> @if (selectWidgetMode === 'bundles') {
<ng-container *ngIf="widgetsBundle; else bundles"> @if (widgetsBundle) {
<ng-container *ngTemplateOutlet="widgets; context:{ fetchFunction: widgetsFetchFunction, filter: widgetsFilter }"></ng-container> <ng-container *ngTemplateOutlet="widgets; context:{ fetchFunction: widgetsFetchFunction, filter: widgetsFilter }"></ng-container>
</ng-container> } @else {
</ng-container> <tb-scroll-grid
<ng-template #allWidgets> [columns]="columns"
[itemSize]="gridWidgetsItemSizeStrategy"
[fetchFunction]="widgetBundlesFetchFunction"
[filter]="widgetsBundleFilter"
[itemCard]="widgetsBundleCard"
[loadingCell]="widgetLoadingCard"
[dataLoading]="loadingWidgetBundles"
[noData]="noWidgetBundles">
</tb-scroll-grid>
<ng-template #widgetsBundleCard let-item="item">
<mat-card class="tb-widget-preview-card flex flex-col gap-2" appearance="raised" (click)="selectBundle($event, item)">
<div class="title-container">
<div title="{{item.name}}" class="widget-title">
{{item.title}}
</div>
<div class="title-items">
@if (isSystem(item)) {
<div class="widget-type">sys</div>
}
<div tb-popover [tbPopoverContent]="item.description" [tbPopoverOverlayStyle]="{maxWidth: '300px'}" tbPopoverShowCloseButton="false" class="info-banner tb-primary-fill"><span>i</span></div>
</div>
</div>
<div class="preview-container">
<div class="preview-spacer"></div>
<img class="preview" [src]="item.image | image: {emptyUrl: '/assets/widget-preview-empty.svg'} | async" alt="{{ item.title }}">
</div>
</mat-card>
</ng-template>
<ng-template #loadingWidgetBundles>
<div class="tb-absolute-fill flex flex-col items-center justify-center">
<span class="mat-headline-5" style="padding-bottom: 20px;">
{{ 'widgets-bundle.loading-widgets-bundles' | translate }}
</span>
<mat-spinner strokeWidth="5"></mat-spinner>
</div>
</ng-template>
<ng-template #noWidgetBundles>
<span translate
class="mat-headline-5 tb-absolute-fill flex items-center justify-center">widgets-bundle.no-widgets-bundles-text</span>
</ng-template>
}
} @else {
<ng-container *ngTemplateOutlet="widgets; context:{ fetchFunction: allWidgetsFetchFunction, filter: allWidgetsFilter }"></ng-container> <ng-container *ngTemplateOutlet="widgets; context:{ fetchFunction: allWidgetsFetchFunction, filter: allWidgetsFilter }"></ng-container>
</ng-template> }
<ng-template #widgets let-fetchFunction="fetchFunction" let-filter="filter"> <ng-template #widgets let-fetchFunction="fetchFunction" let-filter="filter">
<tb-scroll-grid <tb-scroll-grid
[columns]="columns" [columns]="columns"
@ -46,7 +87,9 @@
<div class="widget-type">{{ 'widget.' + item.widgetType + '-short' | translate }}</div> <div class="widget-type">{{ 'widget.' + item.widgetType + '-short' | translate }}</div>
<div tb-popover [tbPopoverContent]="item.description" [tbPopoverOverlayStyle]="{maxWidth: '300px'}" tbPopoverShowCloseButton="false" class="info-banner tb-primary-fill"><span>i</span></div> <div tb-popover [tbPopoverContent]="item.description" [tbPopoverOverlayStyle]="{maxWidth: '300px'}" tbPopoverShowCloseButton="false" class="info-banner tb-primary-fill"><span>i</span></div>
</div> </div>
<div *ngIf="item.deprecated" class="widget-deprecated" translate>widget.deprecated</div> @if (item.deprecated) {
<div class="widget-deprecated" translate>widget.deprecated</div>
}
</div> </div>
</div> </div>
<div class="preview-container"> <div class="preview-container">
@ -64,51 +107,10 @@
</div> </div>
</ng-template> </ng-template>
<ng-template #noWidgets> <ng-template #noWidgets>
<span class="mat-headline-5 tb-absolute-fill flex items-center justify-center">{{(selectWidgetMode === 'bundles' ? 'widgets-bundle.empty' : 'widget.no-widgets-text') | translate}}</span> <span class="mat-headline-5 tb-absolute-fill flex items-center justify-center">{{(selectWidgetMode === 'bundles' ? 'widgets-bundle.empty' : 'widget.no-widgets-text') | translate}}</span>
</ng-template> </ng-template>
</ng-template> </ng-template>
<ng-template #bundles>
<tb-scroll-grid
[columns]="columns"
[itemSize]="gridWidgetsItemSizeStrategy"
[fetchFunction]="widgetBundlesFetchFunction"
[filter]="widgetsBundleFilter"
[itemCard]="widgetsBundleCard"
[loadingCell]="widgetLoadingCard"
[dataLoading]="loadingWidgetBundles"
[noData]="noWidgetBundles">
</tb-scroll-grid>
<ng-template #widgetsBundleCard let-item="item">
<mat-card class="tb-widget-preview-card flex flex-col gap-2" appearance="raised" (click)="selectBundle($event, item)">
<div class="title-container">
<div title="{{item.name}}" class="widget-title">
{{item.title}}
</div>
<div class="title-items">
<div *ngIf="isSystem(item)" class="widget-type">sys</div>
<div tb-popover [tbPopoverContent]="item.description" [tbPopoverOverlayStyle]="{maxWidth: '300px'}" tbPopoverShowCloseButton="false" class="info-banner tb-primary-fill"><span>i</span></div>
</div>
</div>
<div class="preview-container">
<div class="preview-spacer"></div>
<img class="preview" [src]="item.image | image: {emptyUrl: '/assets/widget-preview-empty.svg'} | async" alt="{{ item.title }}">
</div>
</mat-card>
</ng-template>
<ng-template #loadingWidgetBundles>
<div class="tb-absolute-fill flex flex-col items-center justify-center">
<span class="mat-headline-5" style="padding-bottom: 20px;">
{{ 'widgets-bundle.loading-widgets-bundles' | translate }}
</span>
<mat-spinner strokeWidth="5"></mat-spinner>
</div>
</ng-template>
<ng-template #noWidgetBundles>
<span translate
class="mat-headline-5 tb-absolute-fill flex items-center justify-center">widgets-bundle.no-widgets-bundles-text</span>
</ng-template>
</ng-template>
<ng-template #widgetLoadingCard> <ng-template #widgetLoadingCard>
<mat-card class="tb-widget-preview-card loading-cell flex flex-col gap-2" appearance="raised"> <mat-card class="tb-widget-preview-card loading-cell flex flex-col gap-2" appearance="raised">
<div class="title-container"> <div class="title-container">

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

@ -28,56 +28,63 @@
[isDefaultBreakpoint]="isDefaultBreakpoint" [isDefaultBreakpoint]="isDefaultBreakpoint"
formControlName="widgetConfig"> formControlName="widgetConfig">
<div class="tb-widget-config-header-prefix"> <div class="tb-widget-config-header-prefix">
<button mat-button @if (previewMode) {
*ngIf="previewMode" <button mat-button
(click)="previewMode = false"> (click)="previewMode = false">
<mat-icon>chevron_left</mat-icon> <mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }} {{ 'action.back' | translate }}
</button> </button>
<button *ngIf="!previewMode" }
class="gt-sm:!hidden" @if (!previewMode) {
[disabled]="!widgetFormGroup.valid" <button
mat-button color="primary" class="gt-sm:!hidden"
(click)="previewMode = true"> [disabled]="!widgetFormGroup.valid"
<mat-icon>visibility</mat-icon> mat-button color="primary"
{{ 'widget-config.preview' | translate }} (click)="previewMode = true">
</button> <mat-icon>visibility</mat-icon>
{{ 'widget-config.preview' | translate }}
</button>
}
</div> </div>
<div class="tb-widget-config-header-suffix flex flex-row items-center justify-end gap-3 lt-md:flex-1"> <div class="tb-widget-config-header-suffix flex flex-row items-center justify-end gap-3 lt-md:flex-1">
<button *ngIf="!previewMode" @if (!previewMode) {
class="lt-md:!hidden" <button
[disabled]="!widgetFormGroup.valid" class="lt-md:!hidden"
mat-button color="primary" [disabled]="!widgetFormGroup.valid"
(click)="previewMode = true"> mat-button color="primary"
<mat-icon>visibility</mat-icon> (click)="previewMode = true">
{{ 'widget-config.preview' | translate }} <mat-icon>visibility</mat-icon>
</button> {{ 'widget-config.preview' | translate }}
</button>
}
<div class="flex flex-row items-center justify-start gap-3"> <div class="flex flex-row items-center justify-start gap-3">
<button [disabled]="!widgetFormGroup.dirty" <button [disabled]="!widgetFormGroup.dirty"
mat-stroked-button color="primary" mat-stroked-button color="primary"
(click)="onRevertWidgetConfig()"> (click)="onRevertWidgetConfig()">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
{{ 'action.decline' | translate }} {{ 'action.decline' | translate }}
</button> </button>
<button [disabled]="!widgetFormGroup.valid || !widgetFormGroup.dirty" <button [disabled]="!widgetFormGroup.valid || !widgetFormGroup.dirty"
mat-flat-button color="primary" mat-flat-button color="primary"
(click)="onApplyWidgetConfig()"> (click)="onApplyWidgetConfig()">
<mat-icon>done</mat-icon> <mat-icon>done</mat-icon>
{{ 'action.apply' | translate }} {{ 'action.apply' | translate }}
</button> </button>
</div> </div>
</div> </div>
</tb-widget-config> </tb-widget-config>
<div *ngIf="previewMode" class="widget-preview-background"> @if (previewMode) {
<tb-widget-preview class="widget-preview-section" <div class="widget-preview-background">
[aliasController]="aliasController" <tb-widget-preview class="widget-preview-section"
[stateController]="stateController" [aliasController]="aliasController"
[dashboardTimewindow]="dashboard.configuration.timewindow" [stateController]="stateController"
[widget]="widget" [dashboardTimewindow]="dashboard.configuration.timewindow"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config" [widget]="widget"
[previewWidth]="widgetConfig.typeParameters.previewWidth" [widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewHeight]="widgetConfig.typeParameters.previewHeight"> [previewWidth]="widgetConfig.typeParameters.previewWidth"
</tb-widget-preview> [previewHeight]="widgetConfig.typeParameters.previewHeight">
</div> </tb-widget-preview>
</div>
}
</fieldset> </fieldset>
</form> </form>

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

@ -30,22 +30,26 @@
<mat-label translate>layout.breakpoint</mat-label> <mat-label translate>layout.breakpoint</mat-label>
<mat-select formControlName="newBreakpointId" [hideSingleSelectionIndicator]="false"> <mat-select formControlName="newBreakpointId" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(addBreakpointFormGroup.get('newBreakpointId').value) }}</mat-select-trigger> <mat-select-trigger>{{ getName(addBreakpointFormGroup.get('newBreakpointId').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of allowBreakpointIds" [value]="breakpoint" style="min-height:56px;"> @for (breakpoint of allowBreakpointIds; track breakpoint) {
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon> <mat-option [value]="breakpoint" style="min-height:56px;">
<div>{{ getName(breakpoint) }}</div> <mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div> <div>{{ getName(breakpoint) }}</div>
</mat-option> <div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic"> <mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label translate>layout.copy-from</mat-label> <mat-label translate>layout.copy-from</mat-label>
<mat-select formControlName="copyFrom" [hideSingleSelectionIndicator]="false"> <mat-select formControlName="copyFrom" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(addBreakpointFormGroup.get('copyFrom').value) }}</mat-select-trigger> <mat-select-trigger>{{ getName(addBreakpointFormGroup.get('copyFrom').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of selectedBreakpointIds" [value]="breakpoint" style="min-height:56px;"> @for (breakpoint of selectedBreakpointIds; track breakpoint) {
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon> <mat-option [value]="breakpoint" style="min-height:56px;">
<div>{{ getName(breakpoint) }}</div> <mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div> <div>{{ getName(breakpoint) }}</div>
</mat-option> <div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</form> </form>

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

@ -15,28 +15,36 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-progress-cover flex flex-col items-center justify-center" @if (layoutCtx.widgets.isLoading()) {
*ngIf="layoutCtx.widgets.isLoading()"> <div class="tb-progress-cover flex flex-col items-center justify-center"
<mat-spinner color="warn" mode="indeterminate" diameter="100"> >
</mat-spinner> <mat-spinner color="warn" mode="indeterminate" diameter="100">
</div> </mat-spinner>
</div>
}
<div class="mat-content" <div class="mat-content"
style="position: relative; width: 100%; height: 100%;" [tb-hotkeys]="hotKeys" style="position: relative; width: 100%; height: 100%;" [tb-hotkeys]="hotKeys"
[cheatSheet]="dashboardCheatSheet" [cheatSheet]="dashboardCheatSheet"
[style.background-image]="backgroundImage$ | async" [style.background-image]="backgroundImage$ | async"
[style]="dashboardStyle"> [style]="dashboardStyle">
<section *ngIf="layoutCtx.widgets.isEmpty()" @if (layoutCtx.widgets.isEmpty()) {
style="z-index: 1; pointer-events: none;" <section
class="mat-headline-5 tb-absolute-fill flex items-center justify-center"> style="z-index: 1; pointer-events: none;"
<span *ngIf="!isEdit" style="text-align: center;"> class="mat-headline-5 tb-absolute-fill flex items-center justify-center">
{{'dashboard.no-widgets' | translate}} @if (!isEdit) {
</span> <span style="text-align: center;">
<button mat-button *ngIf="isEdit && !widgetEditMode" class="tb-add-new-widget" {{'dashboard.no-widgets' | translate}}
(click)="addWidget($event)"> </span>
<mat-icon class="tb-mat-96">add</mat-icon> }
<span>{{ 'dashboard.add-widget' | translate }}</span> @if (isEdit && !widgetEditMode) {
</button> <button mat-button class="tb-add-new-widget"
</section> (click)="addWidget($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'dashboard.add-widget' | translate }}</span>
</button>
}
</section>
}
<tb-dashboard #dashboard [dashboardStyle]="dashboardStyle" <tb-dashboard #dashboard [dashboardStyle]="dashboardStyle"
[backgroundImage]="backgroundImage$ | async" [backgroundImage]="backgroundImage$ | async"
[widgets]="layoutCtx.widgets" [widgets]="layoutCtx.widgets"

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

@ -20,7 +20,7 @@
<h2 translate>layout.manage</h2> <h2 translate>layout.manage</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
@ -31,201 +31,226 @@
<div class="fixed-title-width" translate>dashboard.layout</div> <div class="fixed-title-width" translate>dashboard.layout</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="layoutType"> <mat-select formControlName="layoutType">
<mat-option *ngFor="let type of layoutTypes" [value]="type"> @for (type of layoutTypes; track type) {
{{ layoutTypeTranslations.get(type) | translate }} <mat-option [value]="type">
</mat-option> {{ layoutTypeTranslations.get(type) | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="!isDividerLayout; else dividerLayout" class="tb-form-panel stroked"> @if (!isDividerLayout) {
<div class="tb-form-table tb-layout-breakpoints"> <div class="tb-form-panel stroked">
<div class="tb-form-table-header no-padding-right"> <div class="tb-form-table tb-layout-breakpoints">
<div class="tb-form-table-header-cell tb-icon"></div> <div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell tb-breakpoint" translate>layout.breakpoints</div> <div class="tb-form-table-header-cell tb-icon"></div>
<div class="tb-form-table-header-cell tb-size" translate>layout.size</div> <div class="tb-form-table-header-cell tb-breakpoint" translate>layout.breakpoints</div>
<div class="tb-form-table-header-cell tb-actions"></div> <div class="tb-form-table-header-cell tb-size" translate>layout.size</div>
</div> <div class="tb-form-table-header-cell tb-actions"></div>
<div class="tb-form-table-body no-gap"> </div>
<ng-container *ngFor="let breakpoint of layoutBreakpoints; let last = last"> <div class="tb-form-table-body no-gap">
<div class="tb-form-table-row no-padding-right"> @for (breakpoint of layoutBreakpoints; track breakpoint; let last = $last) {
<div class="tb-form-table-row-cell tb-icon"> <div class="tb-form-table-row no-padding-right">
<tb-icon>{{ breakpoint.icon }}</tb-icon> <div class="tb-form-table-row-cell tb-icon">
</div> <tb-icon>{{ breakpoint.icon }}</tb-icon>
<div class="tb-form-table-row-cell tb-breakpoint"> </div>
{{ breakpoint.name }} <div class="tb-form-table-row-cell tb-breakpoint">
</div> {{ breakpoint.name }}
<div class="tb-form-table-row-cell tb-size" tbTruncateWithTooltip> </div>
{{ breakpoint.descriptionSize }} <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> </div>
<div class="tb-form-table-row-cell-buttons tb-actions"> @if (!last) {
<mat-divider></mat-divider>
}
}
</div>
</div>
<button mat-button class="tb-add-breakpoint-button flex-1"
matTooltip="{{ 'layout.add-new-breakpoint' | translate }}"
matTooltipPosition="above"
color="primary"
type="button"
[disabled]="!allowBreakpointIds.length"
(click)="addBreakpoint()">
<mat-icon>add</mat-icon>
</button>
</div>
} @else {
<div class="flex flex-col items-center justify-start gap-5">
<mat-button-toggle-group aria-label="Select width value type"
formControlName="type"
[class.!hidden]="!isDividerLayout">
<mat-button-toggle class="flex-1" value="percentage">
{{ 'layout.percentage-width' | translate }}
</mat-button-toggle>
<mat-button-toggle class="flex-1" value="fixed">
{{ 'layout.fixed-width' | translate }}
</mat-button-toggle>
</mat-button-toggle-group>
<div class="tb-layout-preview flex flex-row items-center justify-center">
<div class="tb-layout-preview-container flex flex-row items-center justify-start"
#tooltip="matTooltip"
matTooltip=""
matTooltipPosition="below">
<div [style.flex-basis]="buttonFlexValue() + '%'" [style.max-width]="buttonFlexValue() + '%'"
class="tb-layout-preview-container-main flex flex-1 flex-col items-center justify-start">
<button mat-icon-button
type="button"
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('main')"
class="tb-layout-preview-element"
aria-label="Layout settings"
[class]="layoutButtonClass('main')">
<mat-icon>settings</mat-icon>
</button>
<button type="button"
mat-raised-button
color="primary"
class="tb-layout-button flex-1"
(mouseover)="mainLayoutTooltip.show()"
(mouseleave)="mainLayoutTooltip.hide()"
(click)="setFixedLayout('main')"
[class]="layoutButtonClass('main', true)">
<span [matTooltip]="layoutButtonText('main')"
#mainLayoutTooltip="matTooltip"
matTooltipPosition="above">
{{ (isDividerLayout ? 'layout.left' : 'layout.main') | translate }}
</span>
</button>
@if (showPreviewInputs('main')) {
<div class="tb-layout-preview-element tb-layout-preview-input flex-1">
@if (layoutsFormGroup.get('type').value !== layoutWidthType.FIXED) {
<input
formControlName="leftWidthPercentage"
type="number"
step="1"
min="{{ layoutPercentageSize.MIN }}"
max="{{ layoutPercentageSize.MAX }}"
required>
}
@if (layoutsFormGroup.get('type').value === layoutWidthType.FIXED &&
layoutsFormGroup.get('fixedLayout').value === 'main') {
<input
formControlName="fixedWidth"
type="number"
step="1"
min="{{ layoutFixedSize.MIN }}"
max="{{ layoutFixedSize.MAX }}"
required>
}
</div>
}
</div>
@if (isDividerLayout) {
<div class="flex flex-1 flex-col items-center justify-start" matTooltip="">
<button mat-icon-button <button mat-icon-button
type="button"
matTooltip="{{ 'layout.settings' | translate }}" matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="openLayoutSettings('main', breakpoint.breakpoint)" (click)="openLayoutSettings('right')"
type="button" > class="tb-layout-preview-element"
aria-label="Layout settings"
[class]="layoutButtonClass('right')">
<mat-icon>settings</mat-icon> <mat-icon>settings</mat-icon>
</button> </button>
<button mat-icon-button <button type="button"
matTooltip="{{ 'action.delete' | translate }}" mat-raised-button
matTooltipPosition="above" color="primary"
[disabled]="breakpoint.breakpoint === 'default'" class="tb-layout-button tb-layout-button-right flex-1"
[class.tb-hidden]="breakpoint.breakpoint === 'default'" (mouseover)="rightLayoutTooltip.show()"
(click)="deleteBreakpoint($event, breakpoint.breakpoint)" (mouseleave)="rightLayoutTooltip.hide()"
type="button" > (click)="setFixedLayout('right')"
<tb-icon>delete</tb-icon> [class]="layoutButtonClass('right')">
<span [matTooltip]="layoutButtonText('right')"
#rightLayoutTooltip="matTooltip"
matTooltipPosition="above">
{{ 'layout.right' | translate }}
</span>
</button> </button>
@if (showPreviewInputs('right')) {
<div class="tb-layout-preview-element tb-layout-preview-input flex-1">
@if (layoutsFormGroup.get('type').value !== layoutWidthType.FIXED) {
<input
formControlName="rightWidthPercentage"
type="number"
step="1"
min="{{ layoutPercentageSize.MIN }}"
max="{{ layoutPercentageSize.MAX }}"
required>
}
@if (layoutsFormGroup.get('type').value === layoutWidthType.FIXED) {
<input
formControlName="fixedWidth"
type="number"
step="1"
min="{{ layoutFixedSize.MIN }}"
max="{{ layoutFixedSize.MAX }}"
required>
}
</div>
}
</div> </div>
</div> }
<mat-divider *ngIf="!last"></mat-divider>
</ng-container>
</div>
</div>
<button mat-button class="tb-add-breakpoint-button flex-1"
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 class="flex flex-col items-center justify-start gap-5">
<mat-button-toggle-group aria-label="Select width value type"
formControlName="type"
[class.!hidden]="!isDividerLayout">
<mat-button-toggle class="flex-1" value="percentage">
{{ 'layout.percentage-width' | translate }}
</mat-button-toggle>
<mat-button-toggle class="flex-1" value="fixed">
{{ 'layout.fixed-width' | translate }}
</mat-button-toggle>
</mat-button-toggle-group>
<div class="tb-layout-preview flex flex-row items-center justify-center">
<div class="tb-layout-preview-container flex flex-row items-center justify-start"
#tooltip="matTooltip"
matTooltip=""
matTooltipPosition="below">
<div [style.flex-basis]="buttonFlexValue() + '%'" [style.max-width]="buttonFlexValue() + '%'"
class="tb-layout-preview-container-main flex flex-1 flex-col items-center justify-start">
<button mat-icon-button
type="button"
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('main')"
class="tb-layout-preview-element"
aria-label="Layout settings"
[class]="layoutButtonClass('main')">
<mat-icon>settings</mat-icon>
</button>
<button type="button"
mat-raised-button
color="primary"
class="tb-layout-button flex-1"
(mouseover)="mainLayoutTooltip.show()"
(mouseleave)="mainLayoutTooltip.hide()"
(click)="setFixedLayout('main')"
[class]="layoutButtonClass('main', true)">
<span [matTooltip]="layoutButtonText('main')"
#mainLayoutTooltip="matTooltip"
matTooltipPosition="above">
{{ (isDividerLayout ? 'layout.left' : 'layout.main') | translate }}
</span>
</button>
<div class="tb-layout-preview-element tb-layout-preview-input flex-1" *ngIf="showPreviewInputs('main')">
<input *ngIf="layoutsFormGroup.get('type').value !== layoutWidthType.FIXED"
formControlName="leftWidthPercentage"
type="number"
step="1"
min="{{ layoutPercentageSize.MIN }}"
max="{{ layoutPercentageSize.MAX }}"
required>
<input *ngIf="layoutsFormGroup.get('type').value === layoutWidthType.FIXED &&
layoutsFormGroup.get('fixedLayout').value === 'main'"
formControlName="fixedWidth"
type="number"
step="1"
min="{{ layoutFixedSize.MIN }}"
max="{{ layoutFixedSize.MAX }}"
required>
</div> </div>
</div> </div>
<div class="flex flex-1 flex-col items-center justify-start" *ngIf="isDividerLayout" matTooltip=""> @if (isDividerLayout) {
<button mat-icon-button <div class="flex flex-col items-center justify-center gap-2" style="width: 80%">
type="button" @if (layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE) {
matTooltip="{{ 'layout.settings' | translate }}" <mat-slider
matTooltipPosition="above" step="1"
(click)="openLayoutSettings('right')" min="{{ layoutPercentageSize.MIN }}"
class="tb-layout-preview-element" max="{{ layoutPercentageSize.MAX }}"
aria-label="Layout settings" style="width: 100%;"
[class]="layoutButtonClass('right')"> color="accent"
<mat-icon>settings</mat-icon> discrete
</button> [displayWith]="formatSliderTooltipLabel.bind(this)"><input matSliderThumb formControlName="sliderPercentage"/>
<button type="button" </mat-slider>
mat-raised-button }
color="primary" @if (layoutsFormGroup.get('type').value !== layoutWidthType.PERCENTAGE) {
class="tb-layout-button tb-layout-button-right flex-1" <mat-slider
(mouseover)="rightLayoutTooltip.show()" step="1"
(mouseleave)="rightLayoutTooltip.hide()" min="{{ layoutFixedSize.MIN }}"
(click)="setFixedLayout('right')" max="{{ layoutFixedSize.MAX }}"
[class]="layoutButtonClass('right')"> style="width: 100%;"
<span [matTooltip]="layoutButtonText('right')" color="accent"
#rightLayoutTooltip="matTooltip" discrete
matTooltipPosition="above"> [displayWith]="formatSliderTooltipLabel.bind(this)"><input matSliderThumb formControlName="sliderFixed"/>
{{ 'layout.right' | translate }} </mat-slider>
</span> }
</button> <div class="tb-hint tb-hint-group flex flex-row items-center justify-between" style="width: 100%;">
<div class="tb-layout-preview-element tb-layout-preview-input flex-1" *ngIf="showPreviewInputs('right')"> <label>
<input *ngIf="layoutsFormGroup.get('type').value !== layoutWidthType.FIXED" {{layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE ? layoutPercentageSize.MIN + '%' : layoutFixedSize.MIN + 'px'}}
formControlName="rightWidthPercentage" </label>
type="number" <label>
step="1" {{layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE ? layoutPercentageSize.MAX + '%' : layoutFixedSize.MAX + 'px'}}
min="{{ layoutPercentageSize.MIN }}" </label>
max="{{ layoutPercentageSize.MAX }}" </div>
required>
<input *ngIf="layoutsFormGroup.get('type').value === layoutWidthType.FIXED"
formControlName="fixedWidth"
type="number"
step="1"
min="{{ layoutFixedSize.MIN }}"
max="{{ layoutFixedSize.MAX }}"
required>
</div> </div>
</div> }
</div>
</div>
<div class="flex flex-col items-center justify-center gap-2" *ngIf="isDividerLayout" style="width: 80%">
<mat-slider *ngIf="layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE"
step="1"
min="{{ layoutPercentageSize.MIN }}"
max="{{ layoutPercentageSize.MAX }}"
style="width: 100%;"
color="accent"
discrete
[displayWith]="formatSliderTooltipLabel.bind(this)"><input matSliderThumb formControlName="sliderPercentage"/>
</mat-slider>
<mat-slider *ngIf="layoutsFormGroup.get('type').value !== layoutWidthType.PERCENTAGE"
step="1"
min="{{ layoutFixedSize.MIN }}"
max="{{ layoutFixedSize.MAX }}"
style="width: 100%;"
color="accent"
discrete
[displayWith]="formatSliderTooltipLabel.bind(this)"><input matSliderThumb formControlName="sliderFixed"/>
</mat-slider>
<div class="tb-hint tb-hint-group flex flex-row items-center justify-between" style="width: 100%;">
<label>
{{layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE ? layoutPercentageSize.MIN + '%' : layoutFixedSize.MIN + 'px'}}
</label>
<label>
{{layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE ? layoutPercentageSize.MAX + '%' : layoutFixedSize.MAX + 'px'}}
</label>
</div> </div>
</div> }
</div> </section>
</ng-template>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
<button mat-button <button mat-button

6
ui-ngx/src/app/modules/home/components/dashboard-page/layout/move-widgets-dialog.component.html

@ -25,8 +25,10 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content class="tb-form-panel no-border no-padding" [formGroup]="moveWidgetsFormGroup"> <div mat-dialog-content class="tb-form-panel no-border no-padding" [formGroup]="moveWidgetsFormGroup">
<div class="tb-form-row space-between column-xs"> <div class="tb-form-row space-between column-xs">
<div translate>dashboard.move-by</div> <div translate>dashboard.move-by</div>

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

@ -21,9 +21,11 @@
[(ngModel)]="selectedBreakpoint" [(ngModel)]="selectedBreakpoint"
(ngModelChange)="selectLayoutChanged()"> (ngModelChange)="selectLayoutChanged()">
<mat-select-trigger>{{ getName(selectedBreakpoint) }}</mat-select-trigger> <mat-select-trigger>{{ getName(selectedBreakpoint) }}</mat-select-trigger>
<mat-option *ngFor="let breakpointId of breakpointIds" [value]="breakpointId" style="min-height:56px;"> @for (breakpointId of breakpointIds; track breakpointId) {
<mat-icon><tb-icon>{{ getIcon(breakpointId) }}</tb-icon></mat-icon> <mat-option [value]="breakpointId" style="min-height:56px;">
<div>{{ getName(breakpointId) }}</div> <mat-icon><tb-icon>{{ getIcon(breakpointId) }}</tb-icon></mat-icon>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpointId) }}</div> <div>{{ getName(breakpointId) }}</div>
</mat-option> <div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpointId) }}</div>
</mat-option>
}
</mat-select> </mat-select>

30
ui-ngx/src/app/modules/home/components/dashboard-page/states/dashboard-state-dialog.component.html

@ -25,26 +25,34 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col"> <fieldset [disabled]="isLoading$ | async" class="flex flex-col">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>dashboard.state-name</mat-label> <mat-label translate>dashboard.state-name</mat-label>
<input required matInput formControlName="name"> <input required matInput formControlName="name">
<mat-error *ngIf="stateFormGroup.get('name').hasError('required')"> @if (stateFormGroup.get('name').hasError('required')) {
{{ 'dashboard.state-name-required' | translate }} <mat-error>
</mat-error> {{ 'dashboard.state-name-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>dashboard.state-id</mat-label> <mat-label translate>dashboard.state-id</mat-label>
<input required matInput formControlName="id"> <input required matInput formControlName="id">
<mat-error *ngIf="stateFormGroup.get('id').hasError('required')"> @if (stateFormGroup.get('id').hasError('required')) {
{{ 'dashboard.state-id-required' | translate }} <mat-error>
</mat-error> {{ 'dashboard.state-id-required' | translate }}
<mat-error *ngIf="stateFormGroup.get('id').hasError('stateExists')"> </mat-error>
{{ 'dashboard.state-id-exists' | translate }} }
</mat-error> @if (stateFormGroup.get('id').hasError('stateExists')) {
<mat-error>
{{ 'dashboard.state-id-exists' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="root"> <mat-checkbox formControlName="root">
{{ 'dashboard.is-root-state' | translate }} {{ 'dashboard.is-root-state' | translate }}

10
ui-ngx/src/app/modules/home/components/dashboard-page/states/default-state-controller.component.html

@ -16,8 +16,10 @@
--> -->
<mat-select class="default-state-controller" [class.!hidden]="!displayStateSelection()" <mat-select class="default-state-controller" [class.!hidden]="!displayStateSelection()"
[(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()"> [(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()">
<mat-option *ngFor="let stateKv of states | keyvalue" [value]="stateKv.key"> @for (stateKv of states | keyvalue; track stateKv) {
{{getStateName(stateKv.key, stateKv.value)}} <mat-option [value]="stateKv.key">
</mat-option> {{getStateName(stateKv.key, stateKv.value)}}
</mat-option>
}
</mat-select> </mat-select>

28
ui-ngx/src/app/modules/home/components/dashboard-page/states/entity-state-controller.component.html

@ -16,20 +16,28 @@
--> -->
<div class="entity-state-controller"> <div class="entity-state-controller">
<div *ngIf="!isMobile || stateObject.length === 1" class="flex flex-row items-center justify-start"> @if (!isMobile || stateObject.length === 1) {
<span *ngFor="let state of stateObject; index as i; last as isLast" class="state-entry" <div class="flex flex-row items-center justify-start">
@for (state of stateObject; track state; let i = $index; let isLast = $last) {
<span class="state-entry"
[style.font-weight]="isLast ? 'bold' : 'normal'" [style.font-weight]="isLast ? 'bold' : 'normal'"
[style.cursor]="isLast ? 'default' : 'pointer'" (click)="navigatePrevState(i)"> [style.cursor]="isLast ? 'default' : 'pointer'" (click)="navigatePrevState(i)">
{{getStateName(i)}} {{getStateName(i)}}
<span class='state-divider' [class.!hidden]="isLast"> > </span> <span class='state-divider' [class.!hidden]="isLast"> > </span>
</span> </span>
</div> }
<mat-select *ngIf="isMobile && stateObject.length > 1" </div>
[(ngModel)]="selectedStateIndex" (ngModelChange)="selectedStateIndexChanged()"> }
<mat-option *ngFor="let state of stateObject; index as i" [value]="i"> @if (isMobile && stateObject.length > 1) {
{{getStateName(i)}} <mat-select
</mat-option> [(ngModel)]="selectedStateIndex" (ngModelChange)="selectedStateIndexChanged()">
</mat-select> @for (state of stateObject; track state; let i = $index) {
<mat-option [value]="i">
{{getStateName(i)}}
</mat-option>
}
</mat-select>
}
</div> </div>

10
ui-ngx/src/app/modules/home/components/dashboard-page/widget-types-panel.component.html

@ -16,8 +16,10 @@
--> -->
<div class="mat-content mat-padding flex flex-col"> <div class="mat-content mat-padding flex flex-col">
<mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="type.display" *ngFor="let type of types" @for (type of types; track type) {
(ngModelChange)="update()"> <mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="type.display"
{{ 'widget.' + type.type | translate }} (ngModelChange)="update()">
</mat-checkbox> {{ 'widget.' + type.type | translate }}
</mat-checkbox>
}
</div> </div>

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

@ -24,10 +24,10 @@
</div> </div>
<div id="gridster-parent" <div id="gridster-parent"
class="tb-dashboard-content flex-1" [class.autofill-height]="isAutofillHeight()" class="tb-dashboard-content flex-1" [class.autofill-height]="isAutofillHeight()"
[class.center-vertical]="centerVertical" [class.center-vertical]="centerVertical"
[class.center-horizontal]="centerHorizontal" [class.center-horizontal]="centerHorizontal"
(mousedown)="onDashboardMouseDown($event)" (mousedown)="onDashboardMouseDown($event)"
(tbcontextmenu)="openDashboardContextMenu($event)"> (tbcontextmenu)="openDashboardContextMenu($event)">
<div #dashboardMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed" <div #dashboardMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
[style.left]="dashboardMenuPosition.x" [style.left]="dashboardMenuPosition.x"
[style.top]="dashboardMenuPosition.y" [style.top]="dashboardMenuPosition.y"
@ -36,13 +36,19 @@
<mat-menu #dashboardMenu="matMenu"> <mat-menu #dashboardMenu="matMenu">
<ng-template matMenuContent let-items="items"> <ng-template matMenuContent let-items="items">
<div class="tb-dashboard-context-menu-items"> <div class="tb-dashboard-context-menu-items">
<button mat-menu-item *ngFor="let item of items;" @for (item of items; track item) {
[disabled]="!item.enabled" <button mat-menu-item
(click)="item.action(dashboardContextMenuEvent)"> [disabled]="!item.enabled"
<span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span> (click)="item.action(dashboardContextMenuEvent)">
<mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon> @if (item.shortcut) {
<span translate>{{item.value}}</span> <span class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
</button> }
@if (item.icon) {
<mat-icon>{{item.icon}}</mat-icon>
}
<span translate>{{item.value}}</span>
</button>
}
</div> </div>
</ng-template> </ng-template>
</mat-menu> </mat-menu>
@ -54,37 +60,45 @@
<mat-menu #widgetMenu="matMenu"> <mat-menu #widgetMenu="matMenu">
<ng-template matMenuContent let-items="items" let-widget="widget"> <ng-template matMenuContent let-items="items" let-widget="widget">
<div class="tb-dashboard-context-menu-items"> <div class="tb-dashboard-context-menu-items">
<button mat-menu-item *ngFor="let item of items;" @for (item of items; track item) {
[disabled]="!item.enabled" <button mat-menu-item
(click)="item.action(widgetContextMenuEvent, widget)"> [disabled]="!item.enabled"
<span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span> (click)="item.action(widgetContextMenuEvent, widget)">
<tb-icon matMenuItemIcon *ngIf="item.icon">{{item.icon}}</tb-icon> @if (item.shortcut) {
<span translate>{{item.value}}</span> <span class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
</button> }
@if (item.icon) {
<tb-icon matMenuItemIcon>{{item.icon}}</tb-icon>
}
<span translate>{{item.value}}</span>
</button>
}
</div> </div>
</ng-template> </ng-template>
</mat-menu> </mat-menu>
<div [class]="dashboardClass" id="gridster-background"> <div [class]="dashboardClass" id="gridster-background">
<gridster #gridster id="gridster-child" [options]="gridsterOpts"> <gridster #gridster id="gridster-child" [options]="gridsterOpts">
<gridster-item #gridsterItem [item]="widget" [class]="{'tb-noselect': isEdit}" *ngFor="let widget of dashboardWidgets"> @for (widget of dashboardWidgets; track widget) {
<tb-widget-container <gridster-item #gridsterItem [item]="widget" [class]="{'tb-noselect': isEdit}">
[gridsterItem]="gridsterItem" <tb-widget-container
[widget]="widget" [gridsterItem]="gridsterItem"
[dashboardWidgets]="dashboardWidgets" [widget]="widget"
[dashboardStyle]="dashboardStyle" [dashboardWidgets]="dashboardWidgets"
[backgroundImage]="backgroundImage" [dashboardStyle]="dashboardStyle"
[isEdit]="isEdit" [backgroundImage]="backgroundImage"
[isEditingWidget]="isEditingWidget" [isEdit]="isEdit"
[isPreview]="isPreview" [isEditingWidget]="isEditingWidget"
[isMobile]="isMobileSize" [isPreview]="isPreview"
[isEditActionEnabled]="isEditActionEnabled" [isMobile]="isMobileSize"
[isExportActionEnabled]="isExportActionEnabled" [isEditActionEnabled]="isEditActionEnabled"
[isRemoveActionEnabled]="isRemoveActionEnabled" [isExportActionEnabled]="isExportActionEnabled"
[disableWidgetInteraction]="disableWidgetInteraction" [isRemoveActionEnabled]="isRemoveActionEnabled"
(widgetFullscreenChanged)="onWidgetFullscreenChanged($event)" [disableWidgetInteraction]="disableWidgetInteraction"
(widgetComponentAction)="onWidgetComponentAction($event, widget)"> (widgetFullscreenChanged)="onWidgetFullscreenChanged($event)"
</tb-widget-container> (widgetComponentAction)="onWidgetComponentAction($event, widget)">
</gridster-item> </tb-widget-container>
</gridster-item>
}
</gridster> </gridster>
</div> </div>
</div> </div>

8
ui-ngx/src/app/modules/home/components/dashboard/select-target-layout-dialog.component.html

@ -20,13 +20,15 @@
<h2 translate>layout.select</h2> <h2 translate>layout.select</h2>
<span class="flex-1"></span> <span class="flex-1"></span>
<button mat-icon-button <button mat-icon-button
(click)="cancel()" (click)="cancel()"
type="button"> type="button">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col gap-2"> <fieldset [disabled]="isLoading$ | async" class="flex flex-col gap-2">
<div class="flex flex-row items-center justify-start gap-2"> <div class="flex flex-row items-center justify-start gap-2">

14
ui-ngx/src/app/modules/home/components/dashboard/select-target-state-dialog.component.html

@ -25,16 +25,20 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col"> <fieldset [disabled]="isLoading$ | async" class="flex flex-col">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>dashboard.state</mat-label> <mat-label translate>dashboard.state</mat-label>
<mat-select required formControlName="stateId"> <mat-select required formControlName="stateId">
<mat-option *ngFor="let stateItem of states | keyvalue" [value]="stateItem.key"> @for (stateItem of states | keyvalue; track stateItem) {
{{ stateItem.value.name }} <mat-option [value]="stateItem.key">
</mat-option> {{ stateItem.value.name }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</fieldset> </fieldset>

70
ui-ngx/src/app/modules/home/components/details-panel.component.html

@ -28,41 +28,49 @@
<ng-content select=".header-pane"></ng-content> <ng-content select=".header-pane"></ng-content>
</span> </span>
</div> </div>
<div *ngIf="isShowSearch" [class.!hidden]="!showSearchPane" class="flex flex-row items-center justify-start" style="width: 100%;"> @if (isShowSearch) {
<div [class.!hidden]="!showSearchPane" class="flex flex-row items-center justify-start" style="width: 100%;">
<button mat-icon-button (click)="onToggleSearch()">
<mat-icon class="material-icons">arrow_back</mat-icon>
</button>
<div style="width: 100%; margin: 0 20px">
<ng-content select=".search-pane"></ng-content>
</div>
</div>
}
@if (isShowSearch && !showSearchPane) {
<button mat-icon-button (click)="onToggleSearch()"> <button mat-icon-button (click)="onToggleSearch()">
<mat-icon class="material-icons">arrow_back</mat-icon> <mat-icon class="material-icons">search</mat-icon>
</button> </button>
<div style="width: 100%; margin: 0 20px"> }
<ng-content select=".search-pane"></ng-content>
</div>
</div>
<button mat-icon-button *ngIf="isShowSearch && !showSearchPane" (click)="onToggleSearch()">
<mat-icon class="material-icons">search</mat-icon>
</button>
<ng-content select=".details-buttons"></ng-content> <ng-content select=".details-buttons"></ng-content>
<button *ngIf="showCloseDetails" mat-icon-button (click)="onCloseDetails()"> @if (showCloseDetails) {
<mat-icon class="material-icons">close</mat-icon> <button mat-icon-button (click)="onCloseDetails()">
</button> <mat-icon class="material-icons">close</mat-icon>
</button>
}
</div> </div>
<section *ngIf="!isReadOnly" class="layout-wrap tb-header-buttons flex flex-row"> @if (!isReadOnly) {
<button [disabled]="(isLoading$ | async) || theForm?.invalid || !theForm?.dirty" <section class="layout-wrap tb-header-buttons flex flex-row">
mat-fab <button [disabled]="(isLoading$ | async) || theForm?.invalid || !theForm?.dirty"
matTooltip="{{ 'action.apply-changes' | translate }}" mat-fab
matTooltipPosition="above" matTooltip="{{ 'action.apply-changes' | translate }}"
color="accent" class="tb-btn-header mat-fab-bottom-right" matTooltipPosition="above"
[class.tb-hide]="!isEdit" color="accent" class="tb-btn-header mat-fab-bottom-right"
(click)="onApplyDetails()"> [class.tb-hide]="!isEdit"
<mat-icon class="material-icons">done</mat-icon> (click)="onApplyDetails()">
</button> <mat-icon class="material-icons">done</mat-icon>
<button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)" </button>
mat-fab <button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)"
matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode')) | translate }}" mat-fab
matTooltipPosition="above" matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode')) | translate }}"
color="accent" class="tb-btn-header mat-fab-bottom-right" matTooltipPosition="above"
(click)="onToggleDetailsEditMode()"> color="accent" class="tb-btn-header mat-fab-bottom-right"
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon> (click)="onToggleDetailsEditMode()">
</button> <mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon>
</section> </button>
</section>
}
</mat-toolbar> </mat-toolbar>
</header> </header>
<div class="mat-content flex-1" [style.background-color]="backgroundColor"> <div class="mat-content flex-1" [style.background-color]="backgroundColor">

50
ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html

@ -15,11 +15,10 @@
limitations under the License. limitations under the License.
--> -->
<ng-container *ngIf="panelMode; else componentMode"> @if (panelMode) {
<ng-container *ngTemplateOutlet="deviceFilterPanel"></ng-container> <ng-container *ngTemplateOutlet="deviceFilterPanel"></ng-container>
</ng-container> } @else {
<ng-template #componentMode> @if (buttonMode) {
<ng-container *ngIf="buttonMode; else deviceFilter">
<button color="primary" <button color="primary"
matTooltip="{{buttonDisplayValue}}" matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above" matTooltipPosition="above"
@ -27,8 +26,33 @@
(click)="toggleDeviceFilterPanel($event)"> (click)="toggleDeviceFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}} <mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button> </button>
</ng-container> } @else {
</ng-template> <div class="tb-device-filter-config-component tb-form-panel no-padding no-border" [formGroup]="deviceInfoFilterForm">
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>device-profile.device-profile</div>
<tb-device-profile-autocomplete
class="flex-1"
formControlName="deviceProfileId"
[inlineField]="true"
[displayAllOnEmpty]="true"
(deviceProfileChanged)="deviceProfileChanged($event)"
[editProfileEnabled]="false"
[addNewProfile]="false">
</tb-device-profile-autocomplete>
</div>
<div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>device.device-state</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="active" placeholder="{{ 'device.any' | translate }}">
<mat-option>{{ 'device.any' | translate }}</mat-option>
<mat-option [value]="true">{{ 'device.active' | translate }}</mat-option>
<mat-option [value]="false">{{ 'device.inactive' | translate }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
}
}
<ng-template #deviceFilterPanel> <ng-template #deviceFilterPanel>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()"> <form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="deviceFilter"></ng-container> <ng-container *ngTemplateOutlet="deviceFilter"></ng-container>
@ -59,13 +83,13 @@
<div class="tb-form-row column-xs"> <div class="tb-form-row column-xs">
<div class="fixed-title-width" translate>device-profile.device-profile</div> <div class="fixed-title-width" translate>device-profile.device-profile</div>
<tb-device-profile-autocomplete <tb-device-profile-autocomplete
class="flex-1" class="flex-1"
formControlName="deviceProfileId" formControlName="deviceProfileId"
[inlineField]="true" [inlineField]="true"
[displayAllOnEmpty]="true" [displayAllOnEmpty]="true"
(deviceProfileChanged)="deviceProfileChanged($event)" (deviceProfileChanged)="deviceProfileChanged($event)"
[editProfileEnabled]="false" [editProfileEnabled]="false"
[addNewProfile]="false"> [addNewProfile]="false">
</tb-device-profile-autocomplete> </tb-device-profile-autocomplete>
</div> </div>
<div class="tb-form-row column-xs"> <div class="tb-form-row column-xs">

9
ui-ngx/src/app/modules/home/components/entity/add-entity-dialog.component.html

@ -26,9 +26,12 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> </mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content> <div mat-dialog-content>
<tb-anchor #entityDetailsForm></tb-anchor> <tb-anchor #entityDetailsForm></tb-anchor>
</div> </div>

18
ui-ngx/src/app/modules/home/components/entity/debug/entity-debug-settings-panel.component.html

@ -36,14 +36,16 @@
{{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }} {{ 'debug-settings.all-messages' | translate: { time: (isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched ? (allEnabledUntil | durationLeft) : (maxDebugModeDuration | milliSecondsToTimeString: true : true) } }}
</div> </div>
</mat-slide-toggle> </mat-slide-toggle>
<button mat-icon-button *ngIf="(isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched" @if ((isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched) {
class="tb-mat-20" <button mat-icon-button
matTooltip="{{ 'action.reset' | translate }}" class="tb-mat-20"
matTooltipPosition="above" matTooltip="{{ 'action.reset' | translate }}"
color="primary" matTooltipPosition="above"
(click)="onReset()"> color="primary"
<mat-icon class="material-icons">refresh</mat-icon> (click)="onReset()">
</button> <mat-icon class="material-icons">refresh</mat-icon>
</button>
}
</div> </div>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">

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

@ -16,37 +16,43 @@
--> -->
<mat-drawer-container hasBackdrop="false" class="tb-absolute-fill"> <mat-drawer-container hasBackdrop="false" class="tb-absolute-fill">
<mat-drawer *ngIf="entitiesTableConfig.detailsPanelEnabled" @if (entitiesTableConfig.detailsPanelEnabled) {
class="tb-details-drawer mat-elevation-z4" <mat-drawer
#drawer class="tb-details-drawer mat-elevation-z4"
mode="over" #drawer
position="end" mode="over"
[opened]="isDetailsOpen"> position="end"
<tb-entity-details-panel [opened]="isDetailsOpen">
#entityDetailsPanel <tb-entity-details-panel
[entitiesTableConfig]="entitiesTableConfig" #entityDetailsPanel
[entityId]="isDetailsOpen ? dataSource.currentEntity?.id : null" [entitiesTableConfig]="entitiesTableConfig"
(closeEntityDetails)="isDetailsOpen = false; detailsPanelOpened.emit(isDetailsOpen);" [entityId]="isDetailsOpen ? dataSource.currentEntity?.id : null"
(entityUpdated)="onEntityUpdated($event)" (closeEntityDetails)="isDetailsOpen = false; detailsPanelOpened.emit(isDetailsOpen);"
(entityAction)="onEntityAction($event)" (entityUpdated)="onEntityUpdated($event)"
> (entityAction)="onEntityAction($event)"
</tb-entity-details-panel> >
</mat-drawer> </tb-entity-details-panel>
</mat-drawer>
}
<mat-drawer-content> <mat-drawer-content>
<div class="mat-padding tb-entity-table tb-absolute-fill"> <div class="mat-padding tb-entity-table tb-absolute-fill">
<div class="tb-entity-table-content tb-outlined-border flex flex-col"> <div class="tb-entity-table-content tb-outlined-border flex flex-col">
<mat-toolbar class="mat-mdc-table-toolbar" [class.!hidden]="textSearchMode || !dataSource.selection.isEmpty()"> <mat-toolbar class="mat-mdc-table-toolbar" [class.!hidden]="textSearchMode || !dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<div class="title-container flex flex-row items-center justify-start xs:flex-col xs:items-start xs:justify-center"> <div class="title-container flex flex-row items-center justify-start xs:flex-col xs:items-start xs:justify-center">
<span *ngIf="entitiesTableConfig.tableTitle" class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span> @if (entitiesTableConfig.tableTitle) {
<span class="tb-entity-table-title">{{ entitiesTableConfig.tableTitle }}</span>
}
<tb-anchor #entityTableHeader></tb-anchor> <tb-anchor #entityTableHeader></tb-anchor>
<tb-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow" @if (entitiesTableConfig.useTimePageLink) {
(ngModelChange)="onTimewindowChange()" <tb-timewindow [(ngModel)]="timewindow"
asButton strokedButton historyOnly [forAllTimeEnabled]="entitiesTableConfig.forAllTimeEnabled"></tb-timewindow> (ngModelChange)="onTimewindowChange()"
asButton strokedButton historyOnly [forAllTimeEnabled]="entitiesTableConfig.forAllTimeEnabled"></tb-timewindow>
}
</div> </div>
<span class="flex-1"></span> <span class="flex-1"></span>
<div [class.!hidden]="!addEnabled() || entitiesTableConfig.addAsTextButton"> <div [class.!hidden]="!addEnabled() || entitiesTableConfig.addAsTextButton">
<ng-container *ngIf="!entitiesTableConfig.addActionDescriptors.length; else iconCustom"> @if (!entitiesTableConfig.addActionDescriptors.length) {
<button mat-icon-button <button mat-icon-button
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
(click)="addEntity($event)" (click)="addEntity($event)"
@ -54,48 +60,52 @@
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</ng-container> } @else {
<ng-template #iconCustom> @if (entitiesTableConfig.addActionDescriptors.length === 1) {
<ng-container *ngIf="entitiesTableConfig.addActionDescriptors.length === 1; else iconMenu"> @if (entitiesTableConfig.addActionDescriptors[0].isEnabled()) {
<button *ngIf="entitiesTableConfig.addActionDescriptors[0].isEnabled()" <button
mat-icon-button mat-icon-button
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
(click)="entitiesTableConfig.addActionDescriptors[0].onAction($event)" (click)="entitiesTableConfig.addActionDescriptors[0].onAction($event)"
matTooltip="{{ entitiesTableConfig.addActionDescriptors[0].name }}" matTooltip="{{ entitiesTableConfig.addActionDescriptors[0].name }}"
matTooltipPosition="above"> matTooltipPosition="above">
<tb-icon>{{entitiesTableConfig.addActionDescriptors[0].icon}}</tb-icon> <tb-icon>{{entitiesTableConfig.addActionDescriptors[0].icon}}</tb-icon>
</button> </button>
</ng-container> }
<ng-template #iconMenu> } @else {
<button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ translations.add | translate }}" matTooltip="{{ translations.add | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
[matMenuTriggerFor]="addActionsMenu"> [matMenuTriggerFor]="addActionsMenu">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</ng-template> }
</ng-template> }
</div> </div>
<button mat-icon-button [disabled]="isLoading$ | async" @for (actionDescriptor of headerActionDescriptors; track actionDescriptor) {
[class.!hidden]="!actionDescriptor.isEnabled()" *ngFor="let actionDescriptor of headerActionDescriptors" <button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ actionDescriptor.name }}" [class.!hidden]="!actionDescriptor.isEnabled()"
matTooltipPosition="above" matTooltip="{{ actionDescriptor.name }}"
(click)="actionDescriptor.onAction($event)"> matTooltipPosition="above"
<tb-icon>{{actionDescriptor.icon}}</tb-icon> (click)="actionDescriptor.onAction($event)">
</button> <tb-icon>{{actionDescriptor.icon}}</tb-icon>
</button>
}
<button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()" <button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()"
matTooltip="{{ 'action.refresh' | translate }}" matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>refresh</mat-icon> <mat-icon>refresh</mat-icon>
</button> </button>
<button *ngIf="entitiesTableConfig.searchEnabled" @if (entitiesTableConfig.searchEnabled) {
mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()" <button
matTooltip="{{ translations.search | translate }}" mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
matTooltipPosition="above"> matTooltip="{{ translations.search | translate }}"
<mat-icon>search</mat-icon> matTooltipPosition="above">
</button> <mat-icon>search</mat-icon>
</button>
}
<div [class.!hidden]="!addEnabled() || !entitiesTableConfig.addAsTextButton" class="pl-3" style="--mat-fab-small-container-elevation-shadow: none;"> <div [class.!hidden]="!addEnabled() || !entitiesTableConfig.addAsTextButton" class="pl-3" style="--mat-fab-small-container-elevation-shadow: none;">
<ng-container *ngIf="!entitiesTableConfig.addActionDescriptors.length; else textActions"> @if (!entitiesTableConfig.addActionDescriptors.length) {
<button class="lt-sm:!hidden" <button class="lt-sm:!hidden"
mat-flat-button color="primary" mat-flat-button color="primary"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
@ -111,10 +121,9 @@
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</ng-container> } @else {
<ng-template #textActions> @if (entitiesTableConfig.addActionDescriptors.length === 1) {
<ng-container *ngIf="entitiesTableConfig.addActionDescriptors.length === 1; else textMenu"> @if (entitiesTableConfig.addActionDescriptors[0].isEnabled()) {
<ng-container *ngIf="entitiesTableConfig.addActionDescriptors[0].isEnabled()">
<button class="lt-sm:!hidden" <button class="lt-sm:!hidden"
mat-flat-button color="primary" mat-flat-button color="primary"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
@ -130,9 +139,8 @@
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>{{entitiesTableConfig.addActionDescriptors[0].icon}}</mat-icon> <mat-icon>{{entitiesTableConfig.addActionDescriptors[0].icon}}</mat-icon>
</button> </button>
</ng-container> }
</ng-container> } @else {
<ng-template #textMenu>
<button class="lt-sm:!hidden" <button class="lt-sm:!hidden"
mat-flat-button color="primary" mat-flat-button color="primary"
[disabled]="isLoading$ | async" [disabled]="isLoading$ | async"
@ -148,17 +156,19 @@
matTooltipPosition="above"> matTooltipPosition="above">
<mat-icon>add</mat-icon> <mat-icon>add</mat-icon>
</button> </button>
</ng-template> }
</ng-template> }
</div> </div>
<mat-menu #addActionsMenu="matMenu" xPosition="before"> <mat-menu #addActionsMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let actionDescriptor of entitiesTableConfig.addActionDescriptors" @for (actionDescriptor of entitiesTableConfig.addActionDescriptors; track actionDescriptor) {
[disabled]="isLoading$ | async" <button mat-menu-item
[class.!hidden]="!actionDescriptor.isEnabled()" [disabled]="isLoading$ | async"
(click)="actionDescriptor.onAction($event)"> [class.!hidden]="!actionDescriptor.isEnabled()"
<tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon> (click)="actionDescriptor.onAction($event)">
<span>{{ actionDescriptor.name }}</span> <tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
</button> <span>{{ actionDescriptor.name }}</span>
</button>
}
</mat-menu> </mat-menu>
</div> </div>
</mat-toolbar> </mat-toolbar>
@ -182,24 +192,28 @@
</button> </button>
</div> </div>
</mat-toolbar> </mat-toolbar>
<mat-toolbar *ngIf="entitiesTableConfig.selectionEnabled" class="mat-mdc-table-toolbar" color="primary" [class.!hidden]="dataSource.selection.isEmpty()"> @if (entitiesTableConfig.selectionEnabled) {
<div class="mat-toolbar-tools"> <mat-toolbar class="mat-mdc-table-toolbar" color="primary" [class.!hidden]="dataSource.selection.isEmpty()">
<span class="tb-entity-table-info"> <div class="mat-toolbar-tools">
{{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }} <span class="tb-entity-table-info">
</span> {{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }}
<span class="flex-1"></span> </span>
<button mat-icon-button [disabled]="isLoading$ | async" <span class="flex-1"></span>
[class.!hidden]="!actionDescriptor.isEnabled" *ngFor="let actionDescriptor of groupActionDescriptors" @for (actionDescriptor of groupActionDescriptors; track actionDescriptor) {
matTooltip="{{ actionDescriptor.name }}" <button mat-icon-button [disabled]="isLoading$ | async"
matTooltipPosition="above" [class.!hidden]="!actionDescriptor.isEnabled"
(click)="actionDescriptor.onAction($event, dataSource.selection.selected)"> matTooltip="{{ actionDescriptor.name }}"
<tb-icon>{{actionDescriptor.icon}}</tb-icon> matTooltipPosition="above"
</button> (click)="actionDescriptor.onAction($event, dataSource.selection.selected)">
</div> <tb-icon>{{actionDescriptor.icon}}</tb-icon>
</mat-toolbar> </button>
}
</div>
</mat-toolbar>
}
<div class="table-container flex-1"> <div class="table-container flex-1">
<table mat-table [dataSource]="dataSource" [trackBy]="trackByEntityId" <table mat-table [dataSource]="dataSource" [trackBy]="trackByEntityId"
matSort [matSortActive]="pageLink.sortOrder?.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear> matSort [matSortActive]="pageLink.sortOrder?.property" [matSortDirection]="pageLink.sortDirection()" matSortDisableClear>
<ng-container matColumnDef="select" sticky> <ng-container matColumnDef="select" sticky>
<mat-header-cell *matHeaderCellDef style="width: 40px;"> <mat-header-cell *matHeaderCellDef style="width: 40px;">
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null" <mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
@ -215,82 +229,90 @@
</mat-checkbox> </mat-checkbox>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container [matColumnDef]="column.key" *ngFor="let column of entityColumns; trackBy: trackByColumnKey;"> @for (column of entityColumns; track column.key) {
<mat-header-cell [class.mat-number-cell]="column.isNumberColumn" <ng-container [matColumnDef]="column.key">
[class.lt-lg:!hidden]="column.mobileHide" <mat-header-cell [class.mat-number-cell]="column.isNumberColumn"
*matHeaderCellDef [style]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> [class.lt-lg:!hidden]="column.mobileHide"
*matHeaderCellDef [style]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable">
{{ column.ignoreTranslate ? column.title : (column.title | translate) }} </mat-header-cell> {{ column.ignoreTranslate ? column.title : (column.title | translate) }} </mat-header-cell>
<mat-cell [class.mat-number-cell]="column.isNumberColumn" <mat-cell [class.mat-number-cell]="column.isNumberColumn"
[class.cell-action]="column.actionCell?.type === cellActionType.COPY_BUTTON" [class.cell-action]="column.actionCell?.type === cellActionType.COPY_BUTTON"
[class.lt-lg:!hidden]="column.mobileHide" [class.lt-lg:!hidden]="column.mobileHide"
*matCellDef="let entity; let row = index" *matCellDef="let entity; let row = index"
[matTooltip]="cellTooltip(entity, column, row)" [matTooltip]="cellTooltip(entity, column, row)"
#cellMatTooltip="matTooltip" #cellMatTooltip="matTooltip"
matTooltipPosition="above" matTooltipPosition="above"
[style]="cellStyle(entity, column, row)"> [style]="cellStyle(entity, column, row)">
<ng-container [ngSwitch]="column.type"> @switch (column.type) {
<ng-template [ngSwitchCase]="'link'"> @case ('link') {
<a *ngIf="column.entityURL(entity) as detailsPageURL; else defaultContent" @if (column.entityURL(entity); as detailsPageURL) {
[routerLink]="detailsPageURL" [innerHTML]="cellContent(entity, column, row)" <a
(click)="$event.stopPropagation();"></a> [routerLink]="detailsPageURL" [innerHTML]="cellContent(entity, column, row)"
</ng-template> (click)="$event.stopPropagation();"></a>
<ng-template [ngSwitchCase]="'entityChips'"> } @else {
<tb-entity-chips [entity]="entity" [key]="column.key" <span [innerHTML]="cellContent(entity, column, row)"></span>
[detailsPagePrefixUrl]="column.entityURL ? column.entityURL(entity) : ''"></tb-entity-chips> }
</ng-template> }
<ng-template #defaultContent ngSwitchDefault> @case ('entityChips') {
<span [innerHTML]="cellContent(entity, column, row)"></span> <tb-entity-chips [entity]="entity" [key]="column.key"
</ng-template> [detailsPagePrefixUrl]="column.entityURL ? column.entityURL(entity) : ''"></tb-entity-chips>
</ng-container> }
<ng-template [ngIf]="column.actionCell"> @default {
<ng-container [ngSwitch]="column.actionCell.type"> <span [innerHTML]="cellContent(entity, column, row)"></span>
<ng-template [ngSwitchCase]="cellActionType.COPY_BUTTON"> }
<tb-copy-button }
class="copy-button" @if (column.actionCell) {
[disabled]="isLoading$ | async" @switch (column.actionCell.type) {
[class.!hidden]="!column.actionCell.isEnabled(entity)" @case (cellActionType.COPY_BUTTON) {
[copyText]="column.actionCell.onAction(null, entity)" <tb-copy-button
tooltipText="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}" class="copy-button"
tooltipPosition="above" [disabled]="isLoading$ | async"
(mouseover)="cellMatTooltip.hide()" [class.!hidden]="!column.actionCell.isEnabled(entity)"
(mouseleave)="cellMatTooltip.show()" [copyText]="column.actionCell.onAction(null, entity)"
[icon]="column.actionCell.icon" tooltipText="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
[style]="column.actionCell.style"> tooltipPosition="above"
</tb-copy-button> (mouseover)="cellMatTooltip.hide()"
</ng-template> (mouseleave)="cellMatTooltip.show()"
<ng-template ngSwitchDefault> [icon]="column.actionCell.icon"
<button mat-icon-button [disabled]="isLoading$ | async" [style]="column.actionCell.style">
[class.!hidden]="!column.actionCell.isEnabled(entity)" </tb-copy-button>
matTooltip="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}" }
matTooltipPosition="above" @default {
(click)="column.actionCell.onAction($event, entity)"> <button mat-icon-button [disabled]="isLoading$ | async"
<tb-icon [style]="column.actionCell.style"> [class.!hidden]="!column.actionCell.isEnabled(entity)"
{{column.actionCell.icon}} matTooltip="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
</tb-icon> matTooltipPosition="above"
</button> (click)="column.actionCell.onAction($event, entity)">
</ng-template> <tb-icon [style]="column.actionCell.style">
</ng-container> {{column.actionCell.icon}}
</ng-template> </tb-icon>
</mat-cell> </button>
</ng-container> }
<ng-container [matColumnDef]="column.key" *ngFor="let column of actionColumns; trackBy: trackByColumnKey;"> }
<mat-header-cell *matHeaderCellDef [style]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell> }
<mat-cell *matCellDef="let entity; let row = index" </mat-cell>
[style]="cellStyle(entity, column, row)"> </ng-container>
<button mat-icon-button [disabled]="isLoading$ | async" }
[class.!hidden]="!column.actionDescriptor.isEnabled(entity)" @for (column of actionColumns; track trackByColumnKey($index, column)) {
matTooltip="{{ column.actionDescriptor.nameFunction ? column.actionDescriptor.nameFunction(entity) : column.actionDescriptor.name }}" <ng-container [matColumnDef]="column.key">
matTooltipPosition="above" <mat-header-cell *matHeaderCellDef [style]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable"> {{ column.title | translate }} </mat-header-cell>
(click)="column.actionDescriptor.onAction($event, entity)"> <mat-cell *matCellDef="let entity; let row = index"
<tb-icon [style]="column.actionDescriptor.style"> [style]="cellStyle(entity, column, row)">
{{column.actionDescriptor.iconFunction ? column.actionDescriptor.iconFunction(entity) : column.actionDescriptor.icon}} <button mat-icon-button [disabled]="isLoading$ | async"
</tb-icon> [class.!hidden]="!column.actionDescriptor.isEnabled(entity)"
</button> matTooltip="{{ column.actionDescriptor.nameFunction ? column.actionDescriptor.nameFunction(entity) : column.actionDescriptor.name }}"
</mat-cell> matTooltipPosition="above"
</ng-container> (click)="column.actionDescriptor.onAction($event, entity)">
<tb-icon [style]="column.actionDescriptor.style">
{{column.actionDescriptor.iconFunction ? column.actionDescriptor.iconFunction(entity) : column.actionDescriptor.icon}}
</tb-icon>
</button>
</mat-cell>
</ng-container>
}
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef> <mat-header-cell *matHeaderCellDef>
<ng-container *ngIf="cellActionDescriptors.length"> @if (cellActionDescriptors.length) {
<div class="gt-md:!hidden" style="min-width: 48px;"> <div class="gt-md:!hidden" style="min-width: 48px;">
{{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }} {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
</div> </div>
@ -298,21 +320,22 @@
[style.min-width]="(cellActionDescriptors.length * 48) + 'px'"> [style.min-width]="(cellActionDescriptors.length * 48) + 'px'">
{{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }} {{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
</div> </div>
</ng-container> }
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let entity"> <mat-cell *matCellDef="let entity">
<ng-container *ngIf="cellActionDescriptors.length"> @if (cellActionDescriptors.length) {
<div [class.lt-lg:!hidden]="cellActionDescriptors.length !== 1" class="flex flex-row items-stretch justify-end" <div [class.lt-lg:!hidden]="cellActionDescriptors.length !== 1" class="flex flex-row items-stretch justify-end"
[style.min-width]="(cellActionDescriptors.length * 48) + 'px'"> [style.min-width]="(cellActionDescriptors.length * 48) + 'px'">
<button mat-icon-button [disabled]="(isLoading$ | async) || !actionDescriptor.isEnabled(entity)" @for (actionDescriptor of cellActionDescriptors; track actionDescriptor) {
*ngFor="let actionDescriptor of cellActionDescriptors" <button mat-icon-button [disabled]="(isLoading$ | async) || !actionDescriptor.isEnabled(entity)"
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}" matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, entity)"> (click)="actionDescriptor.onAction($event, entity)">
<tb-icon [style]="actionDescriptor.style"> <tb-icon [style]="actionDescriptor.style">
{{actionDescriptor.iconFunction ? actionDescriptor.iconFunction(entity) : actionDescriptor.icon}} {{actionDescriptor.iconFunction ? actionDescriptor.iconFunction(entity) : actionDescriptor.icon}}
</tb-icon> </tb-icon>
</button> </button>
}
</div> </div>
<div [class.!hidden]="cellActionDescriptors.length === 1" class="gt-md:!hidden"> <div [class.!hidden]="cellActionDescriptors.length === 1" class="gt-md:!hidden">
<button mat-icon-button <button mat-icon-button
@ -321,18 +344,20 @@
<mat-icon class="material-icons">more_vert</mat-icon> <mat-icon class="material-icons">more_vert</mat-icon>
</button> </button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> <mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let actionDescriptor of cellActionDescriptors" @for (actionDescriptor of cellActionDescriptors; track actionDescriptor) {
[disabled]="isLoading$ | async" <button mat-menu-item
[class.!hidden]="!actionDescriptor.isEnabled(entity)" [disabled]="isLoading$ | async"
(click)="actionDescriptor.onAction($event, entity)"> [class.!hidden]="!actionDescriptor.isEnabled(entity)"
<tb-icon matMenuItemIcon [style]="actionDescriptor.style"> (click)="actionDescriptor.onAction($event, entity)">
{{actionDescriptor.iconFunction ? actionDescriptor.iconFunction(entity) : actionDescriptor.icon}} <tb-icon matMenuItemIcon [style]="actionDescriptor.style">
</tb-icon> {{actionDescriptor.iconFunction ? actionDescriptor.iconFunction(entity) : actionDescriptor.icon}}
<span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span> </tb-icon>
</button> <span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span>
</button>
}
</mat-menu> </mat-menu>
</div> </div>
</ng-container> }
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [class.mat-row-select]="selectionEnabled" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row> <mat-header-row [class.mat-row-select]="selectionEnabled" *matHeaderRowDef="displayedColumns; sticky: true"></mat-header-row>
@ -341,21 +366,25 @@
[class.mat-selected]="dataSource.selection.isSelected(entity)" [class.mat-selected]="dataSource.selection.isSelected(entity)"
[class.tb-current-entity]="dataSource.isCurrentEntity(entity)" [class.tb-current-entity]="dataSource.isCurrentEntity(entity)"
[class.tb-pointer]="entitiesTableConfig.rowPointer" [class.tb-pointer]="entitiesTableConfig.rowPointer"
*matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"></mat-row> *matRowDef="let entity; columns: displayedColumns;" (click)="onRowClick($event, entity)"></mat-row>
</table> </table>
<span [class.!hidden]="(isLoading$ | async) || (dataSource.isEmpty() | async) === false || dataSource.dataLoading" <span [class.!hidden]="(isLoading$ | async) || (dataSource.isEmpty() | async) === false || dataSource.dataLoading"
class="no-data-found flex items-center justify-center">{{ translations.noEntities | translate }}</span> class="no-data-found flex items-center justify-center">{{ translations.noEntities | translate }}</span>
<span [class.!hidden]="!dataSource.dataLoading" <span [class.!hidden]="!dataSource.dataLoading"
class="no-data-found flex items-center justify-center">{{ 'common.loading' | translate }}</span> class="no-data-found flex items-center justify-center">{{ 'common.loading' | translate }}</span>
</div> </div>
<mat-divider *ngIf="displayPagination"></mat-divider> @if (displayPagination) {
<mat-paginator *ngIf="displayPagination" <mat-divider></mat-divider>
[length]="dataSource.total() | async" }
[pageIndex]="pageLink.page" @if (displayPagination) {
[pageSize]="pageLink.pageSize" <mat-paginator
[pageSizeOptions]="pageSizeOptions" [length]="dataSource.total() | async"
[hidePageSize]="hidePageSize" [pageIndex]="pageLink.page"
showFirstLastButtons></mat-paginator> [pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator>
}
</div> </div>
</div> </div>
</mat-drawer-content> </mat-drawer-content>

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

@ -49,7 +49,7 @@ import {
CellActionDescriptorType, CellActionDescriptorType,
EntityActionTableColumn, EntityActionTableColumn,
EntityChipsEntityTableColumn, EntityChipsEntityTableColumn,
EntityColumn, EntityColumn, EntityColumnsType, EntityColumnType,
EntityLinkTableColumn, EntityLinkTableColumn,
EntityTableColumn, EntityTableColumn,
EntityTableConfig, EntityTableConfig,
@ -89,8 +89,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
cellActionDescriptors: Array<CellActionDescriptor<BaseData<HasId>>>; cellActionDescriptors: Array<CellActionDescriptor<BaseData<HasId>>>;
actionColumns: Array<EntityActionTableColumn<BaseData<HasId>>>; actionColumns: Array<EntityActionTableColumn<BaseData<HasId>>>;
entityColumns: Array<EntityTableColumn<BaseData<HasId>>>; entityColumns: EntityColumnsType;
displayedColumns: string[]; displayedColumns: string[];
headerCellStyleCache: Array<any> = []; headerCellStyleCache: Array<any> = [];
@ -594,8 +593,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
columnsUpdated(resetData: boolean = false) { columnsUpdated(resetData: boolean = false) {
this.entityColumns = this.entitiesTableConfig.columns.filter( this.entityColumns = this.entitiesTableConfig.columns.filter(
(column) => column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn || (column) => column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn ||
column instanceof EntityChipsEntityTableColumn) column instanceof EntityChipsEntityTableColumn);
.map(column => column as EntityTableColumn<BaseData<HasId>>);
this.actionColumns = this.entitiesTableConfig.columns.filter( this.actionColumns = this.entitiesTableConfig.columns.filter(
(column) => column instanceof EntityActionTableColumn) (column) => column instanceof EntityActionTableColumn)
.map(column => column as EntityActionTableColumn<BaseData<HasId>>); .map(column => column as EntityActionTableColumn<BaseData<HasId>>);
@ -624,8 +622,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors]; this.cellActionDescriptors = [...this.entitiesTableConfig.cellActionDescriptors];
} }
headerCellStyle(column: EntityColumn<BaseData<HasId>>) { headerCellStyle(column: EntityColumnType) {
const index = this.entitiesTableConfig.columns.indexOf(column); const index = this.entitiesTableConfig.columns.indexOf(column as EntityColumn<BaseData<HasId>>);
let res = this.headerCellStyleCache[index]; let res = this.headerCellStyleCache[index];
if (!res) { if (!res) {
const widthStyle: any = {width: column.width}; const widthStyle: any = {width: column.width};
@ -650,7 +648,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
this.cellStyleCache[index] = undefined; this.cellStyleCache[index] = undefined;
} }
cellContent(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) { cellContent(entity: BaseData<HasId>, column: EntityColumnType, row: number) {
if (column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) { if (column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) {
const col = this.entitiesTableConfig.columns.indexOf(column); const col = this.entitiesTableConfig.columns.indexOf(column);
const index = row * this.entitiesTableConfig.columns.length + col; const index = row * this.entitiesTableConfig.columns.length + col;
@ -665,7 +663,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} }
} }
cellTooltip(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) { cellTooltip(entity: BaseData<HasId>, column: EntityColumnType, row: number) {
if (column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) { if (column instanceof EntityTableColumn || column instanceof EntityLinkTableColumn) {
const col = this.entitiesTableConfig.columns.indexOf(column); const col = this.entitiesTableConfig.columns.indexOf(column);
const index = row * this.entitiesTableConfig.columns.length + col; const index = row * this.entitiesTableConfig.columns.length + col;
@ -682,8 +680,8 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
} }
} }
cellStyle(entity: BaseData<HasId>, column: EntityColumn<BaseData<HasId>>, row: number) { cellStyle(entity: BaseData<HasId>, column: EntityColumnType, row: number) {
const col = this.entitiesTableConfig.columns.indexOf(column); const col = this.entitiesTableConfig.columns.indexOf(column as EntityColumn<BaseData<HasId>>);
const index = row * this.entitiesTableConfig.columns.length + col; const index = row * this.entitiesTableConfig.columns.length + col;
let res = this.cellStyleCache[index]; let res = this.cellStyleCache[index];
if (!res) { if (!res) {
@ -702,7 +700,7 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa
return res; return res;
} }
trackByColumnKey(index, column: EntityTableColumn<BaseData<HasId>>) { trackByColumnKey(index, column: EntityTableColumn<BaseData<HasId>> | EntityActionTableColumn<BaseData<HasId>>) {
return column.key; return column.key;
} }

10
ui-ngx/src/app/modules/home/components/entity/entity-chips.component.html

@ -15,7 +15,9 @@
limitations under the License. limitations under the License.
--> -->
<a *ngFor="let subEntity of subEntities" class="tb-entity-chip" @for (subEntity of subEntities; track subEntity) {
routerLink="{{entityDetailsPrefixUrl}}/{{subEntity.id.id}}" (click)="$event.stopPropagation();"> <a class="tb-entity-chip"
{{ subEntity.name | customTranslate }} routerLink="{{entityDetailsPrefixUrl}}/{{subEntity.id.id}}" (click)="$event.stopPropagation();">
</a> {{ subEntity.name | customTranslate }}
</a>
}

40
ui-ngx/src/app/modules/home/components/entity/entity-details-page.component.html

@ -30,25 +30,27 @@
</div> </div>
<div class="tb-help" [tb-help]="helpLinkId()"></div> <div class="tb-help" [tb-help]="helpLinkId()"></div>
<span class="flex-1"></span> <span class="flex-1"></span>
<section *ngIf="!isReadOnly" class="tb-header-button flex flex-row gap-2"> @if (!isReadOnly) {
<button [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty" <section class="tb-header-button flex flex-row gap-2">
mat-fab <button [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty"
matTooltip="{{ 'action.apply-changes' | translate }}" mat-fab
matTooltipPosition="above" matTooltip="{{ 'action.apply-changes' | translate }}"
color="accent" class="tb-btn-header" matTooltipPosition="above"
[class.tb-hide]="!isEdit" color="accent" class="tb-btn-header"
(click)="onApplyDetails()"> [class.tb-hide]="!isEdit"
<mat-icon class="material-icons">done</mat-icon> (click)="onApplyDetails()">
</button> <mat-icon class="material-icons">done</mat-icon>
<button [disabled]="(isLoading$ | async)" </button>
mat-fab <button [disabled]="(isLoading$ | async)"
matTooltip="{{ (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}" mat-fab
matTooltipPosition="above" matTooltip="{{ (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}"
color="accent" class="tb-btn-header" matTooltipPosition="above"
(click)="onToggleDetailsEditMode()"> color="accent" class="tb-btn-header"
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon> (click)="onToggleDetailsEditMode()">
</button> <mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon>
</section> </button>
</section>
}
</div> </div>
</mat-toolbar> </mat-toolbar>
<mat-card-content class="flex-full"> <mat-card-content class="flex-full">

7
ui-ngx/src/app/modules/home/components/entity/entity-filter-view.component.html

@ -16,10 +16,11 @@
--> -->
<div class="tb-entity-filter-view flex flex-col"> <div class="tb-entity-filter-view flex flex-col">
<div *ngIf="!filter || !filter.type; else filterView" class="entity-filter-empty" translate>alias.no-entity-filter-specified</div> @if (!filter || !filter.type) {
<ng-template #filterView> <div class="entity-filter-empty" translate>alias.no-entity-filter-specified</div>
} @else {
<div class="flex flex-col"> <div class="flex flex-col">
<div class="entity-filter-value">{{ filterDisplayValue }}</div> <div class="entity-filter-value">{{ filterDisplayValue }}</div>
</div> </div>
</ng-template> }
</div> </div>

613
ui-ngx/src/app/modules/home/components/entity/entity-filter.component.html

@ -19,315 +19,330 @@
<mat-form-field class="mat-block" appearance="outline"> <mat-form-field class="mat-block" appearance="outline">
<mat-label translate>alias.filter-type</mat-label> <mat-label translate>alias.filter-type</mat-label>
<mat-select required formControlName="type"> <mat-select required formControlName="type">
<mat-option *ngFor="let type of aliasFilterTypes" [value]="type"> @for (type of aliasFilterTypes; track type) {
{{aliasFilterTypeTranslations.get(type) | translate}} <mat-option [value]="type">
</mat-option> {{aliasFilterTypeTranslations.get(type) | translate}}
</mat-option>
}
</mat-select> </mat-select>
<mat-error *ngIf="entityFilterFormGroup.get('type').hasError('required')"> @if (entityFilterFormGroup.get('type').hasError('required')) {
{{ 'alias.filter-type-required' | translate }} <mat-error>
</mat-error> {{ 'alias.filter-type-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<section class="flex flex-col" [formGroup]="filterFormGroup" [ngSwitch]="entityFilterFormGroup.get('type').value"> <section class="flex flex-col" [formGroup]="filterFormGroup">
<ng-template [ngSwitchCase]="aliasFilterType.singleEntity"> @switch (entityFilterFormGroup.get('type').value) {
<tb-entity-select required @case (aliasFilterType.singleEntity) {
appearance="outline" <tb-entity-select required
useAliasEntityTypes="true" appearance="outline"
formControlName="singleEntity">
</tb-entity-select>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityList">
<tb-entity-type-select required
appearance="outline"
showLabel
useAliasEntityTypes="true"
[allowedEntityTypes]="listEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
<tb-entity-list required
appearance="outline"
syncIdsWithDB
labelText="{{'entity.entity-list' | translate}}"
[entityType]="filterFormGroup.get('entityType').value"
formControlName="entityList">
</tb-entity-list>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityName">
<tb-entity-type-select required
appearance="outline"
showLabel
useAliasEntityTypes="true"
[allowedEntityTypes]="listEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>entity.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{'entity.help-text' | translate }}">help</mat-icon>
<input matInput formControlName="entityNameFilter" required>
<mat-error *ngIf="filterFormGroup.get('entityNameFilter').hasError('required')">
{{ 'entity.entity-name-filter-required' | translate }}
</mat-error>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityType">
<tb-entity-type-select required
appearance="outline"
showLabel
useAliasEntityTypes="true"
[allowedEntityTypes]="listEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.stateEntity">
<mat-form-field floatLabel="always" class="mat-block" appearance="outline">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput formControlName="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
</mat-form-field>
<div class="tb-form-panel stroked no-padding-bottom">
<div class="tb-form-panel-title tb-normal">{{ 'alias.default-state-entity' | translate }}</div>
<tb-entity-select appearance="outline"
useAliasEntityTypes="true" useAliasEntityTypes="true"
formControlName="defaultStateEntity"> formControlName="singleEntity">
</tb-entity-select> </tb-entity-select>
</div> }
</ng-template> @case (aliasFilterType.entityList) {
<ng-template [ngSwitchCase]="aliasFilterType.assetType"> <tb-entity-type-select required
<tb-entity-subtype-list required appearance="outline"
floatLabel="always" showLabel
appearance="outline" useAliasEntityTypes="true"
label="{{ 'asset.asset-types' | translate }}" [allowedEntityTypes]="listEntityTypes"
[entityType]="entityType.ASSET" formControlName="entityType">
formControlName="assetTypes"> </tb-entity-type-select>
</tb-entity-subtype-list> <tb-entity-list required
<mat-form-field class="mat-block" appearance="outline"> appearance="outline"
<mat-label translate>asset.name-starts-with</mat-label> syncIdsWithDB
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;" labelText="{{'entity.entity-list' | translate}}"
matTooltip="{{'asset.help-text' | translate }}">help</mat-icon> [entityType]="filterFormGroup.get('entityType').value"
<input matInput formControlName="assetNameFilter"> formControlName="entityList">
</mat-form-field> </tb-entity-list>
</ng-template> }
<ng-template [ngSwitchCase]="aliasFilterType.deviceType"> @case (aliasFilterType.entityName) {
<tb-entity-subtype-list required <tb-entity-type-select required
floatLabel="always" appearance="outline"
appearance="outline" showLabel
label="{{ 'device.device-types' | translate }}" useAliasEntityTypes="true"
[entityType]="entityType.DEVICE" [allowedEntityTypes]="listEntityTypes"
formControlName="deviceTypes"> formControlName="entityType">
</tb-entity-subtype-list> </tb-entity-type-select>
<mat-form-field class="mat-block" appearance="outline"> <mat-form-field class="mat-block" appearance="outline">
<mat-label translate>device.name-starts-with</mat-label> <mat-label translate>entity.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;" <mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{'device.help-text' | translate }}">help</mat-icon> matTooltip="{{'entity.help-text' | translate }}">help</mat-icon>
<input matInput formControlName="deviceNameFilter"> <input matInput formControlName="entityNameFilter" required>
</mat-form-field> @if (filterFormGroup.get('entityNameFilter').hasError('required')) {
</ng-template> <mat-error>
<ng-template [ngSwitchCase]="aliasFilterType.entityViewType"> {{ 'entity.entity-name-filter-required' | translate }}
<tb-entity-subtype-list required </mat-error>
floatLabel="always"
appearance="outline"
label="{{ 'entity-view.entity-view-types' | translate }}"
[entityType]="entityType.ENTITY_VIEW"
formControlName="entityViewTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>entity-view.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{'entity-view.help-text' | translate }}">help</mat-icon>
<input matInput formControlName="entityViewNameFilter">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.edgeType">
<tb-entity-subtype-list required
floatLabel="always"
appearance="outline"
label="{{ 'edge.edge-types' | translate }}"
[entityType]="entityType.EDGE"
formControlName="edgeTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>edge.name-starts-with</mat-label>
<input matInput formControlName="edgeNameFilter">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.relationsQuery">
<section class="tb-form-panel no-border no-padding">
<div class="tb-form-panel stroked" [class.no-padding-bottom]="!filterFormGroup.get('rootStateEntity').value">
<div class="tb-form-panel-title" translate>alias.root-entity</div>
<mat-slide-toggle class="mat-slide" formControlName="rootStateEntity">
{{ 'alias.root-state-entity' | translate }}
</mat-slide-toggle>
@if (filterFormGroup.get('rootStateEntity').value) {
<div class="tb-form-panel no-border no-padding no-gap">
<mat-form-field floatLabel="always" class="mat-block" appearance="outline">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput formControlName="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
</mat-form-field>
<div class="tb-form-panel stroked no-padding-bottom">
<div class="tb-form-panel-title tb-normal">{{ 'alias.default-state-entity' | translate }}</div>
<tb-entity-select appearance="outline"
useAliasEntityTypes="true"
formControlName="defaultStateEntity">
</tb-entity-select>
</div>
</div>
} @else {
<tb-entity-select required
appearance="outline"
useAliasEntityTypes="true"
formControlName="rootEntity">
</tb-entity-select>
} }
</mat-form-field>
}
@case (aliasFilterType.entityType) {
<tb-entity-type-select required
appearance="outline"
showLabel
useAliasEntityTypes="true"
[allowedEntityTypes]="listEntityTypes"
formControlName="entityType">
</tb-entity-type-select>
}
@case (aliasFilterType.stateEntity) {
<mat-form-field floatLabel="always" class="mat-block" appearance="outline">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput formControlName="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
</mat-form-field>
<div class="tb-form-panel stroked no-padding-bottom">
<div class="tb-form-panel-title tb-normal">{{ 'alias.default-state-entity' | translate }}</div>
<tb-entity-select appearance="outline"
useAliasEntityTypes="true"
formControlName="defaultStateEntity">
</tb-entity-select>
</div> </div>
<div class="tb-form-panel stroked"> }
<div class="tb-form-panel-title" translate>alias.query-options</div> @case (aliasFilterType.assetType) {
<div class="flex flex-row gap-4"> <tb-entity-subtype-list required
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic"> floatLabel="always"
<mat-label translate>relation.direction</mat-label> appearance="outline"
<mat-select required formControlName="direction"> label="{{ 'asset.asset-types' | translate }}"
<mat-option *ngFor="let type of directionTypes" [value]="type"> [entityType]="entityType.ASSET"
{{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }} formControlName="assetTypes">
</mat-option> </tb-entity-subtype-list>
</mat-select> <mat-form-field class="mat-block" appearance="outline">
</mat-form-field> <mat-label translate>asset.name-starts-with</mat-label>
<mat-form-field class="flex-1" floatLabel="always" appearance="outline" subscriptSizing="dynamic"> <mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
<mat-label translate>alias.max-relation-level</mat-label> matTooltip="{{'asset.help-text' | translate }}">help</mat-icon>
<input matInput <input matInput formControlName="assetNameFilter">
type="number" </mat-form-field>
min="1" }
step="1" @case (aliasFilterType.deviceType) {
placeholder="{{ 'alias.unlimited-level' | translate }}" <tb-entity-subtype-list required
formControlName="maxLevel"> floatLabel="always"
</mat-form-field> appearance="outline"
label="{{ 'device.device-types' | translate }}"
[entityType]="entityType.DEVICE"
formControlName="deviceTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>device.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{'device.help-text' | translate }}">help</mat-icon>
<input matInput formControlName="deviceNameFilter">
</mat-form-field>
}
@case (aliasFilterType.entityViewType) {
<tb-entity-subtype-list required
floatLabel="always"
appearance="outline"
label="{{ 'entity-view.entity-view-types' | translate }}"
[entityType]="entityType.ENTITY_VIEW"
formControlName="entityViewTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>entity-view.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{'entity-view.help-text' | translate }}">help</mat-icon>
<input matInput formControlName="entityViewNameFilter">
</mat-form-field>
}
@case (aliasFilterType.edgeType) {
<tb-entity-subtype-list required
floatLabel="always"
appearance="outline"
label="{{ 'edge.edge-types' | translate }}"
[entityType]="entityType.EDGE"
formControlName="edgeTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>edge.name-starts-with</mat-label>
<input matInput formControlName="edgeNameFilter">
</mat-form-field>
}
@case (aliasFilterType.relationsQuery) {
<section class="tb-form-panel no-border no-padding">
<div class="tb-form-panel stroked" [class.no-padding-bottom]="!filterFormGroup.get('rootStateEntity').value">
<div class="tb-form-panel-title" translate>alias.root-entity</div>
<mat-slide-toggle class="mat-slide" formControlName="rootStateEntity">
{{ 'alias.root-state-entity' | translate }}
</mat-slide-toggle>
@if (filterFormGroup.get('rootStateEntity').value) {
<div class="tb-form-panel no-border no-padding no-gap">
<mat-form-field floatLabel="always" class="mat-block" appearance="outline">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput formControlName="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
</mat-form-field>
<div class="tb-form-panel stroked no-padding-bottom">
<div class="tb-form-panel-title tb-normal">{{ 'alias.default-state-entity' | translate }}</div>
<tb-entity-select appearance="outline"
useAliasEntityTypes="true"
formControlName="defaultStateEntity">
</tb-entity-select>
</div>
</div>
} @else {
<tb-entity-select required
appearance="outline"
useAliasEntityTypes="true"
formControlName="rootEntity">
</tb-entity-select>
}
</div> </div>
<mat-slide-toggle *ngIf="filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value" <div class="tb-form-panel stroked">
class="mat-slide" formControlName="fetchLastLevelOnly"> <div class="tb-form-panel-title" translate>alias.query-options</div>
{{ 'alias.last-level-relation' | translate }} <div class="flex flex-row gap-4">
</mat-slide-toggle> <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
</div> <mat-label translate>relation.direction</mat-label>
<div class="tb-form-panel stroked"> <mat-select required formControlName="direction">
<div class="tb-form-panel-title" translate>relation.relation-filters</div> @for (type of directionTypes; track type) {
<tb-relation-filters <mat-option [value]="type">
enableNotOption {{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }}
[allowedEntityTypes]="allowedEntityTypes" </mat-option>
formControlName="filters"> }
</tb-relation-filters> </mat-select>
</div>
</section>
</ng-template>
<ng-template [ngSwitchCase]="entityFilterFormGroup.get('type').value === aliasFilterType.assetSearchQuery ||
entityFilterFormGroup.get('type').value === aliasFilterType.deviceSearchQuery ||
entityFilterFormGroup.get('type').value === aliasFilterType.edgeSearchQuery ||
entityFilterFormGroup.get('type').value === aliasFilterType.entityViewSearchQuery ?
entityFilterFormGroup.get('type').value : ''">
<section class="tb-form-panel no-border no-padding">
<div class="tb-form-panel stroked" [class.no-padding-bottom]="!filterFormGroup.get('rootStateEntity').value">
<div class="tb-form-panel-title" translate>alias.root-entity</div>
<mat-slide-toggle class="mat-slide" formControlName="rootStateEntity">
{{ 'alias.root-state-entity' | translate }}
</mat-slide-toggle>
@if (filterFormGroup.get('rootStateEntity').value) {
<div class="tb-form-panel no-border no-padding no-gap">
<mat-form-field floatLabel="always" class="mat-block" appearance="outline">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput formControlName="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
</mat-form-field> </mat-form-field>
<div class="tb-form-panel stroked no-padding-bottom"> <mat-form-field class="flex-1" floatLabel="always" appearance="outline" subscriptSizing="dynamic">
<div class="tb-form-panel-title tb-normal">{{ 'alias.default-state-entity' | translate }}</div> <mat-label translate>alias.max-relation-level</mat-label>
<tb-entity-select appearance="outline" <input matInput
useAliasEntityTypes="true" type="number"
formControlName="defaultStateEntity"> min="1"
</tb-entity-select> step="1"
placeholder="{{ 'alias.unlimited-level' | translate }}"
formControlName="maxLevel">
</mat-form-field>
</div>
@if (filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value) {
<mat-slide-toggle
class="mat-slide" formControlName="fetchLastLevelOnly">
{{ 'alias.last-level-relation' | translate }}
</mat-slide-toggle>
}
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>relation.relation-filters</div>
<tb-relation-filters
enableNotOption
[allowedEntityTypes]="allowedEntityTypes"
formControlName="filters">
</tb-relation-filters>
</div>
</section>
}
@case (entityFilterFormGroup.get('type').value === aliasFilterType.assetSearchQuery ||
entityFilterFormGroup.get('type').value === aliasFilterType.deviceSearchQuery ||
entityFilterFormGroup.get('type').value === aliasFilterType.edgeSearchQuery ||
entityFilterFormGroup.get('type').value === aliasFilterType.entityViewSearchQuery ?
entityFilterFormGroup.get('type').value : '') {
<section class="tb-form-panel no-border no-padding">
<div class="tb-form-panel stroked" [class.no-padding-bottom]="!filterFormGroup.get('rootStateEntity').value">
<div class="tb-form-panel-title" translate>alias.root-entity</div>
<mat-slide-toggle class="mat-slide" formControlName="rootStateEntity">
{{ 'alias.root-state-entity' | translate }}
</mat-slide-toggle>
@if (filterFormGroup.get('rootStateEntity').value) {
<div class="tb-form-panel no-border no-padding no-gap">
<mat-form-field floatLabel="always" class="mat-block" appearance="outline">
<mat-label translate>alias.state-entity-parameter-name</mat-label>
<input matInput formControlName="stateEntityParamName"
placeholder="{{ 'alias.default-entity-parameter-name' | translate }}">
</mat-form-field>
<div class="tb-form-panel stroked no-padding-bottom">
<div class="tb-form-panel-title tb-normal">{{ 'alias.default-state-entity' | translate }}</div>
<tb-entity-select appearance="outline"
useAliasEntityTypes="true"
formControlName="defaultStateEntity">
</tb-entity-select>
</div>
</div> </div>
} @else {
<tb-entity-select required
appearance="outline"
useAliasEntityTypes="true"
formControlName="rootEntity">
</tb-entity-select>
}
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>alias.query-options</div>
<div class="flex flex-row gap-4">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>relation.direction</mat-label>
<mat-select required formControlName="direction">
@for (type of directionTypes; track type) {
<mat-option [value]="type">
{{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-1" floatLabel="always" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alias.max-relation-level</mat-label>
<input matInput
type="number"
min="1"
step="1"
placeholder="{{ 'alias.unlimited-level' | translate }}"
formControlName="maxLevel">
</mat-form-field>
</div> </div>
} @else { @if (filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value) {
<tb-entity-select required <mat-slide-toggle
appearance="outline" class="mat-slide" formControlName="fetchLastLevelOnly">
useAliasEntityTypes="true" {{ 'alias.last-level-relation' | translate }}
formControlName="rootEntity"> </mat-slide-toggle>
</tb-entity-select> }
}
</div>
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>alias.query-options</div>
<div class="flex flex-row gap-4">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>relation.direction</mat-label>
<mat-select required formControlName="direction">
<mat-option *ngFor="let type of directionTypes" [value]="type">
{{ directionTypeTranslations.get(directionTypeEnum[type]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="flex-1" floatLabel="always" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alias.max-relation-level</mat-label>
<input matInput
type="number"
min="1"
step="1"
placeholder="{{ 'alias.unlimited-level' | translate }}"
formControlName="maxLevel">
</mat-form-field>
</div> </div>
<mat-slide-toggle *ngIf="filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value" <div class="tb-form-panel stroked no-padding-bottom">
class="mat-slide" formControlName="fetchLastLevelOnly"> <div class="tb-form-panel-title" translate>relation.relation-filter</div>
{{ 'alias.last-level-relation' | translate }} <tb-relation-type-autocomplete
</mat-slide-toggle> appearance="outline"
</div> subscriptSizing="dynamic"
<div class="tb-form-panel stroked no-padding-bottom"> formControlName="relationType">
<div class="tb-form-panel-title" translate>relation.relation-filter</div> </tb-relation-type-autocomplete>
<tb-relation-type-autocomplete @switch (entityFilterFormGroup.get('type').value) {
appearance="outline" @case (aliasFilterType.assetSearchQuery) {
subscriptSizing="dynamic" <tb-entity-subtype-list
formControlName="relationType"> required
</tb-relation-type-autocomplete> floatLabel="always"
<ng-container [ngSwitch]="entityFilterFormGroup.get('type').value"> appearance="outline"
<ng-template [ngSwitchCase]="aliasFilterType.assetSearchQuery"> label="{{ 'asset.asset-types' | translate }}"
<tb-entity-subtype-list [entityType]="entityType.ASSET"
required formControlName="assetTypes">
floatLabel="always" </tb-entity-subtype-list>
appearance="outline" }
label="{{ 'asset.asset-types' | translate }}" @case (aliasFilterType.deviceSearchQuery) {
[entityType]="entityType.ASSET" <tb-entity-subtype-list
formControlName="assetTypes"> required
</tb-entity-subtype-list> floatLabel="always"
</ng-template> appearance="outline"
<ng-template [ngSwitchCase]="aliasFilterType.deviceSearchQuery"> label="{{ 'device.device-types' | translate }}"
<tb-entity-subtype-list [entityType]="entityType.DEVICE"
required formControlName="deviceTypes">
floatLabel="always" </tb-entity-subtype-list>
appearance="outline" }
label="{{ 'device.device-types' | translate }}" @case (aliasFilterType.edgeSearchQuery) {
[entityType]="entityType.DEVICE" <tb-entity-subtype-list
formControlName="deviceTypes"> required
</tb-entity-subtype-list> floatLabel="always"
</ng-template> appearance="outline"
<ng-template [ngSwitchCase]="aliasFilterType.edgeSearchQuery"> label="{{ 'edge.edge-types' | translate }}"
<tb-entity-subtype-list [entityType]="entityType.EDGE"
required formControlName="edgeTypes">
floatLabel="always" </tb-entity-subtype-list>
appearance="outline" }
label="{{ 'edge.edge-types' | translate }}" @case (aliasFilterType.entityViewSearchQuery) {
[entityType]="entityType.EDGE" <tb-entity-subtype-list
formControlName="edgeTypes"> required
</tb-entity-subtype-list> floatLabel="always"
</ng-template> appearance="outline"
<ng-template [ngSwitchCase]="aliasFilterType.entityViewSearchQuery"> label="{{ 'entity-view.entity-view-types' | translate }}"
<tb-entity-subtype-list [entityType]="entityType.ENTITY_VIEW"
required formControlName="entityViewTypes">
floatLabel="always" </tb-entity-subtype-list>
appearance="outline" }
label="{{ 'entity-view.entity-view-types' | translate }}" }
[entityType]="entityType.ENTITY_VIEW" </div>
formControlName="entityViewTypes"> </section>
</tb-entity-subtype-list> }
</ng-template> }
</ng-container>
</div>
</section>
</ng-template>
</section> </section>
</div> </div>

44
ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.html

@ -17,48 +17,52 @@
--> -->
<form class="tb-filter flex flex-col" [formGroup]="eventFilterFormGroup" (ngSubmit)="update()"> <form class="tb-filter flex flex-col" [formGroup]="eventFilterFormGroup" (ngSubmit)="update()">
<div class="tb-filter-container mat-padding flex flex-col"> <div class="tb-filter-container mat-padding flex flex-col">
<ng-container *ngFor="let column of showColumns"> @for (column of showColumns; track column) {
<ng-container [ngSwitch]="column.key"> @switch (column.key) {
<ng-template [ngSwitchCase]="isSelector(column.key)"> @case (isSelector(column.key)) {
<mat-form-field> <mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label> <mat-label>{{ column.title | translate}}</mat-label>
<mat-select [formControlName]="column.key"> <mat-select [formControlName]="column.key">
<mat-option [value]="">{{ 'event.all-events' | translate}}</mat-option> <mat-option [value]="">{{ 'event.all-events' | translate}}</mat-option>
<mat-option *ngFor="let value of selectorValues(column.key)" [value]="value"> @for (value of selectorValues(column.key); track value) {
{{ value }} <mat-option [value]="value">
</mat-option> {{ value }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</ng-template> }
<ng-template [ngSwitchCase]="isNumberFields(column.key)"> @case (isNumberFields(column.key)) {
<mat-form-field> <mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label> <mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="number" min="0" [name]="column.key" [formControlName]="column.key"> <input matInput type="number" min="0" [name]="column.key" [formControlName]="column.key">
<mat-error *ngIf="eventFilterFormGroup.get(column.key).hasError('min')"> @if (eventFilterFormGroup.get(column.key).hasError('min')) {
{{ 'event.min-value' | translate }} <mat-error>
</mat-error> {{ 'event.min-value' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
</ng-template> }
<ng-template [ngSwitchCase]="'isError'"> @case ('isError') {
<tb-checkbox formControlName="isError" [falseValue]="''" <tb-checkbox formControlName="isError" [falseValue]="''"
(ngModelChange)="changeIsError($event)"> (ngModelChange)="changeIsError($event)">
{{ 'event.has-error' | translate }} {{ 'event.has-error' | translate }}
</tb-checkbox> </tb-checkbox>
</ng-template> }
<ng-template [ngSwitchCase]="'errorStr'"> @case ('errorStr') {
<mat-form-field [class.!hidden]="!showErrorMsgFields()"> <mat-form-field [class.!hidden]="!showErrorMsgFields()">
<mat-label>{{ column.title | translate}}</mat-label> <mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="text" name="errorSearchText" formControlName="errorStr"> <input matInput type="text" name="errorSearchText" formControlName="errorStr">
</mat-form-field> </mat-form-field>
</ng-template> }
<ng-container *ngSwitchDefault> @default {
<mat-form-field> <mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label> <mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="text" [name]="column.key" [formControlName]="column.key"> <input matInput type="text" [name]="column.key" [formControlName]="column.key">
</mat-form-field> </mat-form-field>
</ng-container> }
</ng-container> }
</ng-container> }
</div> </div>
<div class="tb-panel-actions mat-padding flex flex-row items-center justify-end"> <div class="tb-panel-actions mat-padding flex flex-row items-center justify-end">
<button type="button" <button type="button"

8
ui-ngx/src/app/modules/home/components/event/event-table-header.component.html

@ -19,8 +19,10 @@
<mat-label translate>event.event-type</mat-label> <mat-label translate>event.event-type</mat-label>
<mat-select [ngModel]="eventTableConfig.eventType" <mat-select [ngModel]="eventTableConfig.eventType"
(ngModelChange)="eventTypeChanged($event)"> (ngModelChange)="eventTypeChanged($event)">
<mat-option *ngFor="let type of eventTableConfig.eventTypes" [value]="type"> @for (type of eventTableConfig.eventTypes; track type) {
{{ eventTypeTranslationsMap.get(type) | translate }} <mat-option [value]="type">
</mat-option> {{ eventTypeTranslationsMap.get(type) | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>

8
ui-ngx/src/app/modules/home/components/filter/boolean-filter-predicate.component.html

@ -18,9 +18,11 @@
<div class="flex flex-1 flex-row items-start justify-start gap-2" [formGroup]="booleanFilterPredicateFormGroup"> <div class="flex flex-1 flex-row items-start justify-start gap-2" [formGroup]="booleanFilterPredicateFormGroup">
<mat-form-field hideRequiredMarker class="mat-block max-w-30% flex-full"> <mat-form-field hideRequiredMarker class="mat-block max-w-30% flex-full">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of booleanOperations" [value]="operation"> @for (operation of booleanOperations; track operation) {
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}} <mat-option [value]="operation">
</mat-option> {{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource" <tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"

21
ui-ngx/src/app/modules/home/components/filter/complex-filter-predicate-dialog.component.html

@ -30,9 +30,11 @@
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-label translate>filter.operation.operation</mat-label> <mat-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation"> <mat-select required formControlName="operation">
<mat-option *ngFor="let operation of complexOperations" [value]="operation"> @for (operation of complexOperations; track operation) {
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}} <mat-option [value]="operation">
</mat-option> {{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<tb-filter-predicate-list <tb-filter-predicate-list
@ -54,11 +56,12 @@
cdkFocusInitial> cdkFocusInitial>
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!data.readonly) {
*ngIf="!data.readonly" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty"> [disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }} {{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button> </button>
}
</div> </div>
</form> </form>

70
ui-ngx/src/app/modules/home/components/filter/filter-predicate-list.component.html

@ -28,49 +28,57 @@
<div class="flex flex-1 flex-row gap-2"> <div class="flex flex-1 flex-row gap-2">
<div class="flex max-w-30% flex-full flex-row items-center justify-start gap-2"> <div class="flex max-w-30% flex-full flex-row items-center justify-start gap-2">
<label translate class="tb-title no-padding flex-1">filter.operation.operation</label> <label translate class="tb-title no-padding flex-1">filter.operation.operation</label>
<label *ngIf="valueType === valueTypeEnum.STRING" @if (valueType === valueTypeEnum.STRING) {
translate class="tb-title no-padding" style="min-width: 70px;">filter.ignore-case</label> <label
translate class="tb-title no-padding" style="min-width: 70px;">filter.ignore-case</label>
}
</div> </div>
<label translate class="tb-title no-padding max-w-70% flex-full">filter.value</label> <label translate class="tb-title no-padding max-w-70% flex-full">filter.value</label>
</div> </div>
<label *ngIf="displayUserParameters" @if (displayUserParameters) {
translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label> <label
translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label>
}
<span [class.!hidden]="disabled" style="min-width: 40px;">&nbsp;</span> <span [class.!hidden]="disabled" style="min-width: 40px;">&nbsp;</span>
</div> </div>
</div> </div>
<mat-divider style="padding-bottom: 5px;"></mat-divider> <mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list"> <div class="predicate-list">
<div class="flex flex-row items-stretch justify-start h-full min-h-19 max-h-24" @for (predicateControl of predicatesFormArray.controls; track predicateControl) {
formArrayName="predicates" <div class="flex flex-row items-stretch justify-start h-full min-h-19 max-h-24"
*ngFor="let predicateControl of predicatesFormArray.controls; let $index = index"> formArrayName="predicates"
<div class="filters-operation flex max-w-8% flex-full flex-row items-stretch justify-center"> >
<span *ngIf="$index > 0">{{ complexOperationTranslations.get(operation) | translate }}</span> <div class="filters-operation flex max-w-8% flex-full flex-row items-stretch justify-center">
</div> @if ($index > 0) {
<div class="flex max-w-92% min-w-147.5 flex-full flex-col"> <span>{{ complexOperationTranslations.get(operation) | translate }}</span>
<div class="flex flex-1 flex-row items-center justify-start gap-2"> }
<tb-filter-predicate </div>
class="flex-1" <div class="flex max-w-92% min-w-147.5 flex-full flex-col">
[valueType]="valueType" <div class="flex flex-1 flex-row items-center justify-start gap-2">
[displayUserParameters]="displayUserParameters" <tb-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource" class="flex-1"
[onlyUserDynamicSource]="onlyUserDynamicSource" [valueType]="valueType"
[key]="key" [displayUserParameters]="displayUserParameters"
[formControl]="predicateControl"> [allowUserDynamicSource]="allowUserDynamicSource"
</tb-filter-predicate> [onlyUserDynamicSource]="onlyUserDynamicSource"
<button mat-icon-button color="primary" [key]="key"
[class.!hidden]="disabled" [formControl]="predicateControl">
type="button" </tb-filter-predicate>
(click)="removePredicate($index)" <button mat-icon-button color="primary"
matTooltip="{{ 'filter.remove-filter' | translate }}" [class.!hidden]="disabled"
matTooltipPosition="above"> type="button"
<mat-icon>close</mat-icon> (click)="removePredicate($index)"
</button> matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div> </div>
</div> </div>
</div> }
<span [class.!hidden]="!!predicatesFormArray.length" <span [class.!hidden]="!!predicatesFormArray.length"
[class.disabled]="disabled" [class.disabled]="disabled"
class="no-data-found flex items-center justify-center" translate>filter.no-filters</span> class="no-data-found flex items-center justify-center" translate>filter.no-filters</span>
</div> </div>
<div style="margin-top: 16px;" class="flex flex-row gap-2"> <div style="margin-top: 16px;" class="flex flex-row gap-2">
<button mat-button mat-raised-button color="primary" <button mat-button mat-raised-button color="primary"

92
ui-ngx/src/app/modules/home/components/filter/filter-predicate-value.component.html

@ -17,28 +17,30 @@
--> -->
<div class="flex flex-1 flex-row items-center justify-start gap-2" [formGroup]="filterPredicateValueFormGroup"> <div class="flex flex-1 flex-row items-center justify-start gap-2" [formGroup]="filterPredicateValueFormGroup">
<div class="flex flex-1 flex-col" [class.!hidden]="dynamicMode || onlyUserDynamicSource"> <div class="flex flex-1 flex-col" [class.!hidden]="dynamicMode || onlyUserDynamicSource">
<div class="flex flex-col" [ngSwitch]="valueType"> <div class="flex flex-col">
<ng-template [ngSwitchCase]="valueTypeEnum.STRING"> @switch (valueType) {
<mat-form-field hideRequiredMarker class="mat-block"> @case (valueTypeEnum.STRING) {
<input matInput formControlName="defaultValue" placeholder="{{'filter.value' | translate}}"> <mat-form-field hideRequiredMarker class="mat-block">
</mat-form-field> <input matInput formControlName="defaultValue" placeholder="{{'filter.value' | translate}}">
</ng-template> </mat-form-field>
<ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC"> }
<mat-form-field hideRequiredMarker class="mat-block"> @case (valueTypeEnum.NUMERIC) {
<input required type="number" matInput formControlName="defaultValue" <mat-form-field hideRequiredMarker class="mat-block">
placeholder="{{'filter.value' | translate}}"> <input required type="number" matInput formControlName="defaultValue"
</mat-form-field> placeholder="{{'filter.value' | translate}}">
</ng-template> </mat-form-field>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME"> }
<tb-datetime formControlName="defaultValue" @case (valueTypeEnum.DATE_TIME) {
dateText="{{ 'filter.date' | translate }}" <tb-datetime formControlName="defaultValue"
required [showLabel]="false"></tb-datetime> dateText="{{ 'filter.date' | translate }}"
</ng-template> required [showLabel]="false"></tb-datetime>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN"> }
<mat-checkbox formControlName="defaultValue"> @case (valueTypeEnum.BOOLEAN) {
{{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }} <mat-checkbox formControlName="defaultValue">
</mat-checkbox> {{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }}
</ng-template> </mat-checkbox>
}
}
</div> </div>
<div class="tb-hint">{{ hintText | translate }}</div> <div class="tb-hint">{{ hintText | translate }}</div>
</div> </div>
@ -50,9 +52,11 @@
<mat-option [value]="null"> <mat-option [value]="null">
{{'filter.no-dynamic-value' | translate}} {{'filter.no-dynamic-value' | translate}}
</mat-option> </mat-option>
<mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType"> @for (sourceType of dynamicValueSourceTypes; track sourceType) {
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} <mat-option [value]="sourceType">
</mat-option> {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<div class="tb-hint" translate>filter.dynamic-source-type</div> <div class="tb-hint" translate>filter.dynamic-source-type</div>
@ -63,23 +67,27 @@
</mat-form-field> </mat-form-field>
<div class="tb-hint" translate>filter.source-attribute</div> <div class="tb-hint" translate>filter.source-attribute</div>
</div> </div>
<div *ngIf="!allow && inheritMode" @if (!allow && inheritMode) {
class="flex flex-col" <div
style="padding-top: 6px"> class="flex flex-col"
<mat-checkbox formControlName="inherit"> style="padding-top: 6px">
{{ 'filter.inherit-owner' | translate}} <mat-checkbox formControlName="inherit">
</mat-checkbox> {{ 'filter.inherit-owner' | translate}}
<div class="tb-hint" translate>filter.source-attribute-not-set</div> </mat-checkbox>
</div> <div class="tb-hint" translate>filter.source-attribute-not-set</div>
</div>
}
</div> </div>
</div> </div>
<button mat-icon-button *ngIf="!onlyUserDynamicSource" @if (!onlyUserDynamicSource) {
class="mat-elevation-z1 tb-mat-32" <button mat-icon-button
color="primary" class="mat-elevation-z1 tb-mat-32"
type="button" color="primary"
matTooltip="{{ (dynamicMode ? 'filter.switch-to-default-value' : 'filter.switch-to-dynamic-value') | translate }}" type="button"
matTooltipPosition="above" matTooltip="{{ (dynamicMode ? 'filter.switch-to-default-value' : 'filter.switch-to-dynamic-value') | translate }}"
(click)="dynamicMode = !dynamicMode"> matTooltipPosition="above"
<tb-icon class="tb-mat-20">{{ dynamicMode ? 'mdi:numeric' : 'mdi:variable' }}</tb-icon> (click)="dynamicMode = !dynamicMode">
</button> <tb-icon class="tb-mat-20">{{ dynamicMode ? 'mdi:numeric' : 'mdi:variable' }}</tb-icon>
</button>
}
</div> </div>

86
ui-ngx/src/app/modules/home/components/filter/filter-predicate.component.html

@ -16,46 +16,50 @@
--> -->
<div class="flex flex-1 flex-row items-center justify-start gap-2" [formGroup]="filterPredicateFormGroup"> <div class="flex flex-1 flex-row items-center justify-start gap-2" [formGroup]="filterPredicateFormGroup">
<div class="flex flex-1 flex-col" [ngSwitch]="type"> <div class="flex flex-1 flex-col">
<ng-template [ngSwitchCase]="filterPredicateType.STRING"> @switch (type) {
<tb-string-filter-predicate @case (filterPredicateType.STRING) {
[allowUserDynamicSource]="allowUserDynamicSource" <tb-string-filter-predicate
[onlyUserDynamicSource]="onlyUserDynamicSource" [allowUserDynamicSource]="allowUserDynamicSource"
formControlName="predicate"> [onlyUserDynamicSource]="onlyUserDynamicSource"
</tb-string-filter-predicate> formControlName="predicate">
</ng-template> </tb-string-filter-predicate>
<ng-template [ngSwitchCase]="filterPredicateType.NUMERIC"> }
<tb-numeric-filter-predicate @case (filterPredicateType.NUMERIC) {
[allowUserDynamicSource]="allowUserDynamicSource" <tb-numeric-filter-predicate
[onlyUserDynamicSource]="onlyUserDynamicSource" [allowUserDynamicSource]="allowUserDynamicSource"
[valueType]="valueType" [onlyUserDynamicSource]="onlyUserDynamicSource"
formControlName="predicate"> [valueType]="valueType"
</tb-numeric-filter-predicate> formControlName="predicate">
</ng-template> </tb-numeric-filter-predicate>
<ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN"> }
<tb-boolean-filter-predicate @case (filterPredicateType.BOOLEAN) {
[allowUserDynamicSource]="allowUserDynamicSource" <tb-boolean-filter-predicate
[onlyUserDynamicSource]="onlyUserDynamicSource" [allowUserDynamicSource]="allowUserDynamicSource"
formControlName="predicate"> [onlyUserDynamicSource]="onlyUserDynamicSource"
</tb-boolean-filter-predicate> formControlName="predicate">
</ng-template> </tb-boolean-filter-predicate>
<ng-template [ngSwitchCase]="filterPredicateType.COMPLEX"> }
<tb-complex-filter-predicate @case (filterPredicateType.COMPLEX) {
[allowUserDynamicSource]="allowUserDynamicSource" <tb-complex-filter-predicate
[onlyUserDynamicSource]="onlyUserDynamicSource" [allowUserDynamicSource]="allowUserDynamicSource"
[key]="key" [onlyUserDynamicSource]="onlyUserDynamicSource"
[valueType]="valueType" [key]="key"
[displayUserParameters]="displayUserParameters" [valueType]="valueType"
formControlName="predicate"> [displayUserParameters]="displayUserParameters"
</tb-complex-filter-predicate> formControlName="predicate">
</ng-template> </tb-complex-filter-predicate>
}
}
</div> </div>
<tb-filter-user-info *ngIf="type !== filterPredicateType.COMPLEX && displayUserParameters" @if (type !== filterPredicateType.COMPLEX && displayUserParameters) {
style="width: 60px;" <tb-filter-user-info
class="flex flex-row items-stretch justify-center" style="width: 60px;"
[valueType]="valueType" class="flex flex-row items-stretch justify-center"
[operation]="filterPredicateFormGroup.get('predicate').value?.operation" [valueType]="valueType"
[key]="key" [operation]="filterPredicateFormGroup.get('predicate').value?.operation"
formControlName="userInfo"> [key]="key"
</tb-filter-user-info> formControlName="userInfo">
</tb-filter-user-info>
}
</div> </div>

25
ui-ngx/src/app/modules/home/components/filter/filter-user-info-dialog.component.html

@ -50,11 +50,13 @@
<input matInput type="number" formControlName="order"> <input matInput type="number" formControlName="order">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-form-row space-between" *ngIf="showUniInput"> @if (showUniInput) {
<div class="min-w-44" translate>filter.unit</div> <div class="tb-form-row space-between">
<tb-unit-input supportsUnitConversion formControlName="unit" class="medium-width flex-xs"> <div class="min-w-44" translate>filter.unit</div>
</tb-unit-input> <tb-unit-input supportsUnitConversion formControlName="unit" class="medium-width flex-xs">
</div> </tb-unit-input>
</div>
}
</div> </div>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
@ -65,11 +67,12 @@
cdkFocusInitial> cdkFocusInitial>
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!data.readonly) {
*ngIf="!data.readonly" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || filterUserInfoFormGroup.invalid || !filterUserInfoFormGroup.dirty"> [disabled]="(isLoading$ | async) || filterUserInfoFormGroup.invalid || !filterUserInfoFormGroup.dirty">
{{ 'action.update' | translate }} {{ 'action.update' | translate }}
</button> </button>
}
</div> </div>
</form> </form>

94
ui-ngx/src/app/modules/home/components/filter/filters-dialog.component.html

@ -25,62 +25,68 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div> </mat-progress-bar>
}
@if ((isLoading$ | async) === false) {
<div style="height: 4px;"></div>
}
<div mat-dialog-content> <div mat-dialog-content>
<div class="tb-filters-header flex flex-row items-center justify-start"> <div class="tb-filters-header flex flex-row items-center justify-start">
<span class="max-w-5% flex-full"></span> <span class="max-w-5% flex-full"></span>
<div class="flex max-w-95% flex-full flex-row items-center justify-start"> <div class="flex max-w-95% flex-full flex-row items-center justify-start">
<div class="tb-header-label flex-1" translate>filter.filter</div> <div class="tb-header-label flex-1" translate>filter.filter</div>
<div class="tb-header-label flex min-w-20 max-w-20 flex-1 basis-20 flex-col items-center justify-center" translate <div class="tb-header-label flex min-w-20 max-w-20 flex-1 basis-20 flex-col items-center justify-center" translate
style="padding-left: 5px;">filter.editable</div> style="padding-left: 5px;">filter.editable</div>
<div style="min-width: 132px;"></div> <div style="min-width: 132px;"></div>
</div> </div>
</div> </div>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<mat-divider></mat-divider> <mat-divider></mat-divider>
<div class="flex flex-1 flex-row items-center justify-start" @for (filterControl of filtersFormArray().controls; track filterControl) {
formArrayName="filters" <div class="flex flex-1 flex-row items-center justify-start"
*ngFor="let filterControl of filtersFormArray().controls; let $index = index"> formArrayName="filters"
<span class="max-w-5% flex-full">{{$index + 1}}.</span> >
<div class="mat-elevation-z4 tb-filter flex max-w-95% flex-full flex-row items-center justify-start"> <span class="max-w-5% flex-full">{{$index + 1}}.</span>
<mat-label class="flex-1">{{filterControl.get('filter').value}}</mat-label> <div class="mat-elevation-z4 tb-filter flex max-w-95% flex-full flex-row items-center justify-start">
<section style="padding-left: 10px;" <mat-label class="flex-1">{{filterControl.get('filter').value}}</mat-label>
class="tb-editable-switch flex min-w-20 max-w-20 flex-1 basis-20 flex-col items-center justify-center"> <section style="padding-left: 10px;"
<mat-slide-toggle class="editable-switch" class="tb-editable-switch flex min-w-20 max-w-20 flex-1 basis-20 flex-col items-center justify-center">
[formControl]="filterControl.get('editable')"> <mat-slide-toggle class="editable-switch"
</mat-slide-toggle> [formControl]="filterControl.get('editable')">
</section> </mat-slide-toggle>
<button [disabled]="isLoading$ | async" </section>
mat-icon-button color="primary" <button [disabled]="isLoading$ | async"
style="min-width: 40px;" mat-icon-button color="primary"
type="button" style="min-width: 40px;"
(click)="editFilter($index)" type="button"
matTooltip="{{ 'filter.edit' | translate }}" (click)="editFilter($index)"
matTooltipPosition="above"> matTooltip="{{ 'filter.edit' | translate }}"
<mat-icon>edit</mat-icon> matTooltipPosition="above">
</button> <mat-icon>edit</mat-icon>
<button [disabled]="isLoading$ | async" </button>
mat-icon-button color="primary" <button [disabled]="isLoading$ | async"
style="min-width: 48px;" mat-icon-button color="primary"
type="button" style="min-width: 48px;"
(click)="duplicateFilter($index)" type="button"
matTooltip="{{ 'filter.duplicate-filter-action' | translate }}" (click)="duplicateFilter($index)"
matTooltipPosition="above"> matTooltip="{{ 'filter.duplicate-filter-action' | translate }}"
<mat-icon>content_copy</mat-icon> matTooltipPosition="above">
</button> <mat-icon>content_copy</mat-icon>
<button [disabled]="isLoading$ | async" </button>
mat-icon-button color="primary" <button [disabled]="isLoading$ | async"
style="min-width: 40px;" mat-icon-button color="primary"
type="button" style="min-width: 40px;"
(click)="removeFilter($index)" type="button"
matTooltip="{{ 'filter.remove-filter' | translate }}" (click)="removeFilter($index)"
matTooltipPosition="above"> matTooltip="{{ 'filter.remove-filter' | translate }}"
<mat-icon>close</mat-icon> matTooltipPosition="above">
</button> <mat-icon>close</mat-icon>
</button>
</div>
</div> </div>
</div> }
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>

30
ui-ngx/src/app/modules/home/components/filter/filters-edit-panel.component.html

@ -16,18 +16,22 @@
--> -->
<div class="mat-content mat-padding flex flex-col"> <div class="mat-content mat-padding flex flex-col">
<div *ngFor="let filter of filtersInfo | keyvalue; let last = last"> @for (filter of filtersInfo | keyvalue; track filter; let last = $last) {
<div class="flex flex-row items-center justify-start"> <div>
<mat-label class="flex-1">{{filter.value.filter}}</mat-label> <div class="flex flex-row items-center justify-start">
<button mat-icon-button color="primary" <mat-label class="flex-1">{{filter.value.filter}}</mat-label>
style="min-width: 40px;" <button mat-icon-button color="primary"
type="button" style="min-width: 40px;"
(click)="editFilter(filter.key, filter.value)" type="button"
matTooltip="{{ 'filter.edit' | translate }}" (click)="editFilter(filter.key, filter.value)"
matTooltipPosition="above"> matTooltip="{{ 'filter.edit' | translate }}"
<mat-icon>edit</mat-icon> matTooltipPosition="above">
</button> <mat-icon>edit</mat-icon>
</button>
</div>
@if (!last) {
<mat-divider></mat-divider>
}
</div> </div>
<mat-divider *ngIf="!last"></mat-divider> }
</div>
</div> </div>

26
ui-ngx/src/app/modules/home/components/filter/filters-edit.component.html

@ -15,18 +15,20 @@
limitations under the License. limitations under the License.
--> -->
<section class="tb-filters-edit flex flex-row items-center justify-start" *ngIf="hasEditableFilters"> @if (hasEditableFilters) {
<button mat-icon-button <section class="tb-filters-edit flex flex-row items-center justify-start">
cdkOverlayOrigin #filtersEditPanelOrigin="cdkOverlayOrigin" <button mat-icon-button
cdkOverlayOrigin #filtersEditPanelOrigin="cdkOverlayOrigin"
(click)="openEditMode()"
matTooltip="{{ 'filter.filters' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon>filter_list</mat-icon>
</button>
<span class="lt-lg:!hidden"
(click)="openEditMode()" (click)="openEditMode()"
matTooltip="{{ 'filter.filters' | translate }}" matTooltip="{{ 'filter.filters' | translate }}"
[matTooltipPosition]="tooltipPosition"> [matTooltipPosition]="tooltipPosition">
<mat-icon>filter_list</mat-icon> {{ 'filter.filters' | translate }}
</button> </span>
<span class="lt-lg:!hidden" </section>
(click)="openEditMode()" }
matTooltip="{{ 'filter.filters' | translate }}"
[matTooltipPosition]="tooltipPosition">
{{ 'filter.filters' | translate }}
</span>
</section>

143
ui-ngx/src/app/modules/home/components/filter/key-filter-dialog.component.html

@ -33,13 +33,15 @@
'max-w-45%': isConstantKeyType, 'max-w-45%': isConstantKeyType,
'max-w-70%': !isConstantKeyType 'max-w-70%': !isConstantKeyType
}" }"
formGroupName="key"> formGroupName="key">
<mat-form-field class="mat-block max-w-40% flex-full"> <mat-form-field class="mat-block max-w-40% flex-full">
<mat-label translate>filter.key-type.key-type</mat-label> <mat-label translate>filter.key-type.key-type</mat-label>
<mat-select required formControlName="type"> <mat-select required formControlName="type">
<mat-option *ngFor="let type of entityKeyTypes" [value]="type"> @for (type of entityKeyTypes; track type) {
{{entityKeyTypeTranslations.get(type) | translate}} <mat-option [value]="type">
</mat-option> {{entityKeyTypeTranslations.get(type) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block max-w-60% flex-full"> <mat-form-field class="mat-block max-w-60% flex-full">
@ -49,20 +51,26 @@
(focusin)="onFocus()" (focusin)="onFocus()"
[matAutocomplete]="keyName" [matAutocomplete]="keyName"
[matAutocompleteDisabled]="!showAutocomplete"> [matAutocompleteDisabled]="!showAutocomplete">
<button *ngIf="keyFilterFormGroup.get('key.key').value && showAutocomplete" @if (keyFilterFormGroup.get('key.key').value && showAutocomplete) {
type="button" <button
matSuffix mat-icon-button aria-label="Clear" type="button"
(click)="clear()"> matSuffix mat-icon-button aria-label="Clear"
<mat-icon class="material-icons">close</mat-icon> (click)="clear()">
</button> <mat-icon class="material-icons">close</mat-icon>
</button>
}
<mat-autocomplete autoActiveFirstOption #keyName="matAutocomplete"> <mat-autocomplete autoActiveFirstOption #keyName="matAutocomplete">
<mat-option *ngFor="let keyName of filteredKeysName | async" [value]="keyName"> @for (keyName of filteredKeysName | async; track keyName) {
<span [innerHTML]="keyName | highlight:searchText"></span> <mat-option [value]="keyName">
</mat-option> <span [innerHTML]="keyName | highlight:searchText"></span>
</mat-option>
}
</mat-autocomplete> </mat-autocomplete>
<mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')"> @if (keyFilterFormGroup.get('key.key').hasError('required')) {
{{ 'filter.key-name-required' | translate }} <mat-error>
</mat-error> {{ 'filter.key-name-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
</section> </section>
<mat-form-field class="mat-block tb-value-type flex-full" <mat-form-field class="mat-block tb-value-type flex-full"
@ -76,50 +84,60 @@
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.icon }}"></mat-icon> <mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }}</span> <span>{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }}</span>
</mat-select-trigger> </mat-select-trigger>
<mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType"> @for (valueType of entityKeyValueTypesKeys; track valueType) {
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon> <mat-option [value]="valueType">
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span> <mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
</mat-option> <span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
</mat-option>
}
</mat-select> </mat-select>
<mat-error *ngIf="keyFilterFormGroup.get('valueType').hasError('required')"> @if (keyFilterFormGroup.get('valueType').hasError('required')) {
{{ 'filter.value-type-required' | translate }} <mat-error>
</mat-error> {{ 'filter.value-type-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<div class="flex-1" *ngIf="isConstantKeyType"> @if (isConstantKeyType) {
<div class="flex flex-col" [ngSwitch]="keyFilterFormGroup.get('valueType').value"> <div class="flex-1">
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.STRING"> <div class="flex flex-col">
<mat-form-field class="mat-block"> @switch (keyFilterFormGroup.get('valueType').value) {
<mat-label translate>filter.value</mat-label> @case (entityKeyValueTypeEnum.STRING) {
<input matInput required formControlName="value" placeholder="{{'filter.value' | translate}}"> <mat-form-field class="mat-block">
</mat-form-field> <mat-label translate>filter.value</mat-label>
</ng-template> <input matInput required formControlName="value" placeholder="{{'filter.value' | translate}}">
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.NUMERIC"> </mat-form-field>
<mat-form-field class="mat-block"> }
<mat-label translate>filter.value</mat-label> @case (entityKeyValueTypeEnum.NUMERIC) {
<input required type="number" matInput formControlName="value"> <mat-form-field class="mat-block">
</mat-form-field> <mat-label translate>filter.value</mat-label>
</ng-template> <input required type="number" matInput formControlName="value">
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.DATE_TIME"> </mat-form-field>
<tb-datetime formControlName="value" }
dateText="{{ 'filter.date' | translate }}" @case (entityKeyValueTypeEnum.DATE_TIME) {
required></tb-datetime> <tb-datetime formControlName="value"
</ng-template> dateText="{{ 'filter.date' | translate }}"
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.BOOLEAN"> required></tb-datetime>
<mat-checkbox formControlName="value" style="margin-top: 20px"> }
{{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }} @case (entityKeyValueTypeEnum.BOOLEAN) {
</mat-checkbox> <mat-checkbox formControlName="value" style="margin-top: 20px">
</ng-template> {{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
}
}
</div>
</div> </div>
</div> }
</section> </section>
<tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value" @if (keyFilterFormGroup.get('valueType').value) {
[allowUserDynamicSource]="data.allowUserDynamicSource" <tb-filter-predicate-list
[onlyUserDynamicSource]="isConstantKeyType" [allowUserDynamicSource]="data.allowUserDynamicSource"
[displayUserParameters]="data.displayUserParameters" [onlyUserDynamicSource]="isConstantKeyType"
[valueType]="keyFilterFormGroup.get('valueType').value" [displayUserParameters]="data.displayUserParameters"
[key]="keyFilterFormGroup.get('key.key').value" [valueType]="keyFilterFormGroup.get('valueType').value"
formControlName="predicates"> [key]="keyFilterFormGroup.get('key.key').value"
</tb-filter-predicate-list> formControlName="predicates">
</tb-filter-predicate-list>
}
</fieldset> </fieldset>
</div> </div>
<div mat-dialog-actions class="flex items-center justify-end"> <div mat-dialog-actions class="flex items-center justify-end">
@ -130,11 +148,12 @@
cdkFocusInitial> cdkFocusInitial>
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }} {{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!data.readonly) {
type="submit" <button mat-raised-button color="primary"
*ngIf="!data.readonly" type="submit"
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty"> [disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }} {{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button> </button>
}
</div> </div>
</form> </form>

8
ui-ngx/src/app/modules/home/components/filter/numeric-filter-predicate.component.html

@ -18,9 +18,11 @@
<div class="flex flex-1 flex-row items-start justify-start gap-2" [formGroup]="numericFilterPredicateFormGroup"> <div class="flex flex-1 flex-row items-start justify-start gap-2" [formGroup]="numericFilterPredicateFormGroup">
<mat-form-field hideRequiredMarker class="mat-block max-w-30% flex-full"> <mat-form-field hideRequiredMarker class="mat-block max-w-30% flex-full">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of numericOperations" [value]="operation"> @for (operation of numericOperations; track operation) {
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}} <mat-option [value]="operation">
</mat-option> {{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource" <tb-filter-predicate-value [allowUserDynamicSource]="allowUserDynamicSource"

8
ui-ngx/src/app/modules/home/components/filter/string-filter-predicate.component.html

@ -19,9 +19,11 @@
<div class="flex max-w-30% flex-full flex-row items-center justify-start gap-2"> <div class="flex max-w-30% flex-full flex-row items-center justify-start gap-2">
<mat-form-field hideRequiredMarker class="mat-block flex-1"> <mat-form-field hideRequiredMarker class="mat-block flex-1">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}"> <mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of stringOperations" [value]="operation"> @for (operation of stringOperations; track operation) {
{{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}} <mat-option [value]="operation">
</mat-option> {{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-checkbox class="flex flex-row items-stretch justify-center" formControlName="ignoreCase" style="min-width: 70px;"> <mat-checkbox class="flex flex-row items-stretch justify-center" formControlName="ignoreCase" style="min-width: 70px;">

20
ui-ngx/src/app/modules/home/components/notification/send-notification-button.component.html

@ -15,12 +15,14 @@
limitations under the License. limitations under the License.
--> -->
<div *ngIf="show()" class="flex flex-row items-end justify-end" style="height: 100%; padding: 0 16px;"> @if (show()) {
<button class="lt-md:!hidden" mat-flat-button color="primary" (click)="sendNotification($event)"> <div class="flex flex-row items-end justify-end" style="height: 100%; padding: 0 16px;">
<mat-icon>send</mat-icon> <button class="lt-md:!hidden" mat-flat-button color="primary" (click)="sendNotification($event)">
<span translate>notification.send-notification</span> <mat-icon>send</mat-icon>
</button> <span translate>notification.send-notification</span>
<button class="mat-elevation-z0 gt-sm:!hidden" mat-mini-fab color="primary" (click)="sendNotification($event)"> </button>
<mat-icon>send</mat-icon> <button class="mat-elevation-z0 gt-sm:!hidden" mat-mini-fab color="primary" (click)="sendNotification($event)">
</button> <mat-icon>send</mat-icon>
</div> </button>
</div>
}

75
ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.html

@ -17,43 +17,52 @@
--> -->
<section class="flex flex-row items-center justify-between" style="margin: 0 10px 4px; min-height: 36px"> <section class="flex flex-row items-center justify-between" style="margin: 0 10px 4px; min-height: 36px">
<div style="font-weight: 500; letter-spacing: .25px" translate>notification.notification</div> <div style="font-weight: 500; letter-spacing: .25px" translate>notification.notification</div>
<button mat-button color="primary" @if ((notifications$ | async)?.length) {
*ngIf="(notifications$ | async)?.length" <button mat-button color="primary"
(click)="markAsAllRead($event)"> (click)="markAsAllRead($event)">
{{ 'notification.mark-all-as-read' | translate }} {{ 'notification.mark-all-as-read' | translate }}
</button> </button>
}
</section> </section>
<mat-divider></mat-divider> <mat-divider></mat-divider>
<ng-container *ngIf="notifications$ | async; else loadingNotification"> @if (notifications$ | async) {
<div *ngIf="(notifications$ | async).length; else emptyNotification" style="overflow: auto"> @if ((notifications$ | async).length) {
<section style="min-height: 100px; overflow: auto; padding: 6px 0;"> <div style="overflow: auto">
<div *ngFor="let notification of (notifications$ | async); let last = last; trackBy: trackById"> <section style="min-height: 100px; overflow: auto; padding: 6px 0;">
<tb-notification [notification]="notification" @for (notification of (notifications$ | async); track notification.id.id; let last = $last) {
[onClose]="onClose" <div>
(markAsRead)="markAsRead($event)"> <tb-notification [notification]="notification"
</tb-notification> [onClose]="onClose"
<mat-divider *ngIf="!last" style="margin: 4px"></mat-divider> (markAsRead)="markAsRead($event)">
</div> </tb-notification>
@if (!last) {
<mat-divider style="margin: 4px"></mat-divider>
}
</div>
}
</section>
</div>
} @else {
<div class="tb-no-notification-svg-color" style="margin: 20px 24%">
<svg height="100%" preserveAspectRatio="xMidYMid meet" viewBox="0 0 149 156" width="100%">
<use [attr.xlink:href]="'assets/notification-bell.svg#CHECK_ICON'"></use>
</svg>
</div>
<span style="text-align: center; margin-bottom: 12px" translate>notification.no-notifications-yet</span>
}
@if ((notifications$ | async).length) {
<mat-divider></mat-divider>
}
@if ((notifications$ | async).length) {
<section class="flex items-center justify-center">
<button class="flex-1" mat-button color="primary" (click)="viewAll($event)">
{{ 'notification.view-all' | translate }}
</button>
</section> </section>
</div> }
<mat-divider *ngIf="(notifications$ | async).length"></mat-divider> } @else {
<section class="flex items-center justify-center" *ngIf="(notifications$ | async).length">
<button class="flex-1" mat-button color="primary" (click)="viewAll($event)">
{{ 'notification.view-all' | translate }}
</button>
</section>
</ng-container>
<ng-template #emptyNotification>
<div class="tb-no-notification-svg-color" style="margin: 20px 24%">
<svg height="100%" preserveAspectRatio="xMidYMid meet" viewBox="0 0 149 156" width="100%">
<use [attr.xlink:href]="'assets/notification-bell.svg#CHECK_ICON'"></use>
</svg>
</div>
<span style="text-align: center; margin-bottom: 12px" translate>notification.no-notifications-yet</span>
</ng-template>
<ng-template #loadingNotification>
<div class="tb-no-data-available" style="margin: 20px; gap: 16px;"> <div class="tb-no-data-available" style="margin: 20px; gap: 16px;">
<mat-spinner color="accent" diameter="65" strokeWidth="4"></mat-spinner> <mat-spinner color="accent" diameter="65" strokeWidth="4"></mat-spinner>
<div class="tb-no-data-text" translate>notification.loading-notifications</div> <div class="tb-no-data-text" translate>notification.loading-notifications</div>
</div> </div>
</ng-template> }

8
ui-ngx/src/app/modules/home/components/notification/show-notification-popover.component.ts

@ -46,8 +46,8 @@ export class ShowNotificationPopoverComponent extends PageComponent implements O
private notificationSubscriber = NotificationSubscriber.createNotificationsSubscription(this.notificationWsService, this.zone, 6); private notificationSubscriber = NotificationSubscriber.createNotificationsSubscription(this.notificationWsService, this.zone, 6);
notifications$: Observable<Notification[]> = this.notificationSubscriber.notifications$.pipe( notifications$: Observable<Notification[]> = (this.notificationSubscriber.notifications$ as Observable<Notification[] | null>).pipe(
filter(value => Array.isArray(value)), filter((value): value is Notification[] => Array.isArray(value)),
shareReplay(1), shareReplay(1),
tap(() => setTimeout(() => this.cd.markForCheck())) tap(() => setTimeout(() => this.cd.markForCheck()))
); );
@ -91,8 +91,4 @@ export class ShowNotificationPopoverComponent extends PageComponent implements O
this.onClose(); this.onClose();
this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {}); this.router.navigateByUrl(this.router.parseUrl('/notification/inbox')).then(() => {});
} }
trackById(index: number, item: NotificationRequest): string {
return item.id.id;
}
} }

56
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-duration-predicate-value.component.html

@ -22,18 +22,26 @@
step="1" min="1" max="2147483647" step="1" min="1" max="2147483647"
formControlName="defaultValue" formControlName="defaultValue"
placeholder="{{ defaultValuePlaceholder | translate }}"> placeholder="{{ defaultValuePlaceholder | translate }}">
<mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('required')"> @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('required')) {
{{ defaultValueRequiredError | translate }} <mat-error>
</mat-error> {{ defaultValueRequiredError | translate }}
<mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('min')"> </mat-error>
{{ defaultValueRangeError | translate }} }
</mat-error> @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('min')) {
<mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('max')"> <mat-error>
{{ defaultValueRangeError | translate }} {{ defaultValueRangeError | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('pattern')"> }
{{ defaultValuePatternError | translate }} @if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('max')) {
</mat-error> <mat-error>
{{ defaultValueRangeError | translate }}
</mat-error>
}
@if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('pattern')) {
<mat-error>
{{ defaultValuePatternError | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-1 flex-col" [class.!hidden]="!dynamicMode"> <div class="flex flex-1 flex-col" [class.!hidden]="!dynamicMode">
@ -44,9 +52,11 @@
<mat-option [value]="null"> <mat-option [value]="null">
{{'filter.no-dynamic-value' | translate}} {{'filter.no-dynamic-value' | translate}}
</mat-option> </mat-option>
<mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType"> @for (sourceType of dynamicValueSourceTypes; track sourceType) {
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} <mat-option [value]="sourceType">
</mat-option> {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -55,13 +65,15 @@
<input matInput formControlName="sourceAttribute" placeholder="{{'filter.source-attribute' | translate}}"> <input matInput formControlName="sourceAttribute" placeholder="{{'filter.source-attribute' | translate}}">
</mat-form-field> </mat-form-field>
</div> </div>
<div *ngIf="inheritMode" @if (inheritMode) {
class="flex flex-col" <div
style="padding-top: 6px"> class="flex flex-col"
<mat-checkbox formControlName="inherit"> style="padding-top: 6px">
{{ 'filter.inherit-owner' | translate}} <mat-checkbox formControlName="inherit">
</mat-checkbox> {{ 'filter.inherit-owner' | translate}}
</div> </mat-checkbox>
</div>
}
</div> </div>
</div> </div>
<button mat-icon-button <button mat-icon-button

8
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-dynamic-value.component.html

@ -35,9 +35,11 @@
<mat-option [value]="null"> <mat-option [value]="null">
{{'filter.no-dynamic-value' | translate}} {{'filter.no-dynamic-value' | translate}}
</mat-option> </mat-option>
<mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType"> @for (sourceType of dynamicValueSourceTypes; track sourceType) {
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}} <mat-option [value]="sourceType">
</mat-option> {{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

83
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition-dialog.component.html

@ -25,8 +25,10 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
@ -41,35 +43,47 @@
<mat-form-field class="mat-block" hideRequiredMarker> <mat-form-field class="mat-block" hideRequiredMarker>
<mat-label translate>device-profile.condition-type</mat-label> <mat-label translate>device-profile.condition-type</mat-label>
<mat-select formControlName="type" required> <mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType"> @for (alarmConditionType of alarmConditionTypes; track alarmConditionType) {
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }} <mat-option [value]="alarmConditionType">
</mat-option> {{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
}
</mat-select> </mat-select>
<mat-error *ngIf="conditionFormGroup.get('spec.type').hasError('required')"> @if (conditionFormGroup.get('spec.type').hasError('required')) {
{{ 'device-profile.condition-type-required' | translate }} <mat-error>
</mat-error> {{ 'device-profile.condition-type-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<div class="flex flex-row xs:flex-col gt-xs:gap-4" *ngIf="conditionFormGroup.get('spec.type').value != AlarmConditionType.SIMPLE"> @if (conditionFormGroup.get('spec.type').value != AlarmConditionType.SIMPLE) {
<tb-alarm-duration-predicate-value <div class="flex flex-row xs:flex-col gt-xs:gap-4">
class="flex flex-1 flex-row" <tb-alarm-duration-predicate-value
formControlName="predicate" class="flex flex-1 flex-row"
[alarmConditionType]="conditionFormGroup.get('spec.type').value" formControlName="predicate"
> [alarmConditionType]="conditionFormGroup.get('spec.type').value"
</tb-alarm-duration-predicate-value> >
<div class="max-w-23% flex-full" *ngIf="conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION"> </tb-alarm-duration-predicate-value>
<mat-form-field class="mat-block" subscriptSizing="dynamic"> @if (conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION) {
<mat-select formControlName="unit" required <div class="max-w-23% flex-full">
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}"> <mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> <mat-select formControlName="unit" required
{{ timeUnitTranslations.get(timeUnit) | translate }} placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
</mat-option> @for (timeUnit of timeUnits; track timeUnit) {
</mat-select> <mat-option [value]="timeUnit">
<mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')"> {{ timeUnitTranslations.get(timeUnit) | translate }}
{{ 'device-profile.condition-duration-time-unit-required' | translate }} </mat-option>
</mat-error> }
</mat-form-field> </mat-select>
@if (conditionFormGroup.get('spec.unit').hasError('required')) {
<mat-error>
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
}
</mat-form-field>
</div>
}
</div> </div>
</div> }
</section> </section>
</div> </div>
</fieldset> </fieldset>
@ -81,11 +95,12 @@
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} {{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!readonly) {
*ngIf="!readonly" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty"> [disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }} {{ 'action.save' | translate }}
</button> </button>
}
</div> </div>
</form> </form>

6
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule-condition.component.html

@ -23,8 +23,10 @@
required required
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}"> addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
</tb-filter-text> </tb-filter-text>
<span *ngIf="specText" class="tb-alarm-rule-condition-spec" [class.disabled]="disabled" [innerHTML]="specText"> @if (specText) {
</span> <span class="tb-alarm-rule-condition-spec" [class.disabled]="disabled" [innerHTML]="specText">
</span>
}
</div> </div>
<button mat-icon-button <button mat-icon-button
[color]="conditionSet() ? 'primary' : 'warn'" [color]="conditionSet() ? 'primary' : 'warn'"

58
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-rule.component.html

@ -20,30 +20,36 @@
</tb-alarm-rule-condition> </tb-alarm-rule-condition>
<tb-alarm-schedule-info formControlName="schedule"> <tb-alarm-schedule-info formControlName="schedule">
</tb-alarm-schedule-info> </tb-alarm-schedule-info>
<div *ngIf="!disabled || alarmRuleFormGroup.get('alarmDetails').value" class="flex flex-row items-center justify-start"> @if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<span class="tb-alarm-rule-details title" (click)="openEditDetailsDialog($event)"> <div class="flex flex-row items-center justify-start">
{{ ('device-profile.alarm-rule-additional-info' | translate) + ': ' }} <span class="tb-alarm-rule-details title" (click)="openEditDetailsDialog($event)">
</span> {{ ('device-profile.alarm-rule-additional-info' | translate) + ': ' }}
<span *ngIf="alarmRuleFormGroup.get('alarmDetails').value" class="tb-alarm-rule-details" </span>
(click)="openEditDetailsDialog($event)" @if (alarmRuleFormGroup.get('alarmDetails').value) {
[innerHTML]="alarmDetailsText"></span> <span class="tb-alarm-rule-details"
<button mat-icon-button color="primary" (click)="openEditDetailsDialog($event)"
type="button" [innerHTML]="alarmDetailsText"></span>
(click)="openEditDetailsDialog($event)" }
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}" <button mat-icon-button color="primary"
matTooltipPosition="above"> type="button"
<mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon> (click)="openEditDetailsDialog($event)"
</button> matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
</div> matTooltipPosition="above">
<div *ngIf="!disabled || alarmRuleFormGroup.get('dashboardId').value" class="flex flex-col items-start justify-start gt-sm:flex-row gt-sm:items-center"> <mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon>
<span class="tb-alarm-rule-dashboard title"> </button>
{{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }} </div>
</span> }
<tb-dashboard-autocomplete class="tb-alarm-rule-dashboard dashboard" @if (!disabled || alarmRuleFormGroup.get('dashboardId').value) {
label="" <div class="flex flex-col items-start justify-start gt-sm:flex-row gt-sm:items-center">
placeholder="{{ 'device-profile.alarm-rule-no-mobile-dashboard' | translate }}" <span class="tb-alarm-rule-dashboard title">
formControlName="dashboardId"> {{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }}
<span tb-hint>{{'device-profile.alarm-rule-mobile-dashboard-hint' | translate}}</span> </span>
</tb-dashboard-autocomplete> <tb-dashboard-autocomplete class="tb-alarm-rule-dashboard dashboard"
</div> label=""
placeholder="{{ 'device-profile.alarm-rule-no-mobile-dashboard' | translate }}"
formControlName="dashboardId">
<span tb-hint>{{'device-profile.alarm-rule-mobile-dashboard-hint' | translate}}</span>
</tb-dashboard-autocomplete>
</div>
}
</div> </div>

19
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule-dialog.component.html

@ -25,8 +25,10 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content> <div mat-dialog-content>
<tb-alarm-schedule <tb-alarm-schedule
formControlName="alarmSchedule"> formControlName="alarmSchedule">
@ -39,11 +41,12 @@
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} {{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!readonly) {
*ngIf="!readonly" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty"> [disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
{{ 'action.save' | translate }} {{ 'action.save' | translate }}
</button> </button>
}
</div> </div>
</form> </form>

175
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.html

@ -18,91 +18,106 @@
<section [formGroup]="alarmScheduleForm" class="flex flex-col"> <section [formGroup]="alarmScheduleForm" class="flex flex-col">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<mat-select formControlName="type" required placeholder="{{ 'device-profile.schedule-type' | translate }}"> <mat-select formControlName="type" required placeholder="{{ 'device-profile.schedule-type' | translate }}">
<mat-option *ngFor="let alarmScheduleType of alarmScheduleTypes" [value]="alarmScheduleType"> @for (alarmScheduleType of alarmScheduleTypes; track alarmScheduleType) {
{{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }} <mat-option [value]="alarmScheduleType">
</mat-option> {{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }}
</mat-option>
}
</mat-select> </mat-select>
<mat-error *ngIf="alarmScheduleForm.get('type').hasError('required')"> @if (alarmScheduleForm.get('type').hasError('required')) {
{{ 'device-profile.schedule-type-required' | translate }} <mat-error>
</mat-error> {{ 'device-profile.schedule-type-required' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<div *ngIf="alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME"> @if (alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME) {
<tb-timezone-select <div>
userTimezoneByDefault <tb-timezone-select
required userTimezoneByDefault
formControlName="timezone"> required
</tb-timezone-select> formControlName="timezone">
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME"> </tb-timezone-select>
<tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_specific_schedule_format'></tb-alarm-dynamic-value> @if (alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME) {
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div> <section>
<div class="flex flex-col gap-2 gt-md:flex-row"> <tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_specific_schedule_format'></tb-alarm-dynamic-value>
<div class="flex flex-row gap-2"> <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<mat-checkbox *ngFor="let day of firstRowDays" [formControl]="weeklyRepeatControl(day)"> <div class="flex flex-col gap-2 gt-md:flex-row">
{{ dayOfWeekTranslationsArray[day] | translate }} <div class="flex flex-row gap-2">
</mat-checkbox> @for (day of firstRowDays; track day) {
</div> <mat-checkbox [formControl]="weeklyRepeatControl(day)">
<div class="flex flex-row gap-2"> {{ dayOfWeekTranslationsArray[day] | translate }}
<mat-checkbox *ngFor="let day of secondRowDays" [formControl]="weeklyRepeatControl(day)"> </mat-checkbox>
{{ dayOfWeekTranslationsArray[day] | translate }} }
</mat-checkbox> </div>
</div> <div class="flex flex-row gap-2">
</div> @for (day of secondRowDays; track day) {
<mat-checkbox [formControl]="weeklyRepeatControl(day)">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
}
</div>
</div>
<tb-error style="display: block;" [error]="alarmScheduleForm.get('daysOfWeek').hasError('dayOfWeeks') <tb-error style="display: block;" [error]="alarmScheduleForm.get('daysOfWeek').hasError('dayOfWeeks')
? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error> ? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error>
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div> <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-time</div>
<div class="flex flex-row xs:flex-col gt-xs:gap-2"> <div class="flex flex-row xs:flex-col gt-xs:gap-2">
<div class="flex flex-row gap-2 gt-md:flex-1"> <div class="flex flex-row gap-2 gt-md:flex-1">
<mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5"> <mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5">
<mat-label translate>device-profile.schedule-time-from</mat-label> <mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker"> <input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5"> <mat-form-field class="flex-1 sm:min-w-37.5 sm:max-w-37.5 sm:basis-37.5 md:min-w-37.5 md:max-w-37.5 md:basis-37.5">
<mat-label translate>device-profile.schedule-time-to</mat-label> <mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle> <mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker> <mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker"> <input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-1 items-center justify-center" style="margin: auto"> <div class="flex flex-1 items-center justify-center" style="margin: auto">
<div style="text-align: center" <div style="text-align: center"
[innerHTML]="getSchedulerRangeText(alarmScheduleForm)"> [innerHTML]="getSchedulerRangeText(alarmScheduleForm)">
</div> </div>
</div> </div>
</div>
</section>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM">
<tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_custom_schedule_format'></tb-alarm-dynamic-value>
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div *ngFor="let day of allDays" class="flex flex-col gap-4" formArrayName="items">
<div class="flex flex-row items-center justify-start gap-2 xs:flex-col xs:items-start xs:justify-center" [formGroupName]="''+day">
<mat-checkbox formControlName="enabled" class="max-w-17% flex-full" (change)="changeCustomScheduler($event, day)">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
<div class="flex flex-1 flex-row gap-2">
<mat-form-field class="xs:flex-1 sm:min-w-25 sm:max-w-25 sm:flex-1 sm:basis-25 md:min-w-25 md:max-w-25 md:flex-1 md:basis-25">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
</mat-form-field>
<mat-form-field class="xs:flex-1 sm:min-w-25 sm:max-w-25 sm:flex-1 sm:basis-25 md:min-w-25 md:max-w-25 md:flex-1 md:basis-25">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field>
</div> </div>
<div class="flex flex-1 items-center justify-center" </section>
style="text-align: center" }
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))"> @if (alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM) {
</div> <section>
</div> <tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_custom_schedule_format'></tb-alarm-dynamic-value>
</div> <div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
@for (day of allDays; track day) {
<div class="flex flex-col gap-4" formArrayName="items">
<div class="flex flex-row items-center justify-start gap-2 xs:flex-col xs:items-start xs:justify-center" [formGroupName]="''+day">
<mat-checkbox formControlName="enabled" class="max-w-17% flex-full" (change)="changeCustomScheduler($event, day)">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
<div class="flex flex-1 flex-row gap-2">
<mat-form-field class="xs:flex-1 sm:min-w-25 sm:max-w-25 sm:flex-1 sm:basis-25 md:min-w-25 md:max-w-25 md:flex-1 md:basis-25">
<mat-label translate>device-profile.schedule-time-from</mat-label>
<mat-datetimepicker-toggle [for]="startTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startsOn" [matDatetimepicker]="startTimePicker">
</mat-form-field>
<mat-form-field class="xs:flex-1 sm:min-w-25 sm:max-w-25 sm:flex-1 sm:basis-25 md:min-w-25 md:max-w-25 md:flex-1 md:basis-25">
<mat-label translate>device-profile.schedule-time-to</mat-label>
<mat-datetimepicker-toggle [for]="endTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endTimePicker type="time" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endsOn" [matDatetimepicker]="endTimePicker">
</mat-form-field>
</div>
<div class="flex flex-1 items-center justify-center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div>
</div>
</div>
}
<tb-error style="display: block;" [error]="alarmScheduleForm.get('items').hasError('dayOfWeeks') <tb-error style="display: block;" [error]="alarmScheduleForm.get('items').hasError('dayOfWeeks')
? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error> ? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error>
</section> </section>
</div> }
</div>
}
</section> </section>

2
ui-ngx/src/app/modules/home/components/profile/alarm/alarm-schedule.component.ts

@ -63,7 +63,7 @@ export class AlarmScheduleComponent implements ControlValueAccessor, Validator,
alarmScheduleForm: UntypedFormGroup; alarmScheduleForm: UntypedFormGroup;
alarmScheduleTypes = Object.keys(AlarmScheduleType); alarmScheduleTypes = Object.values(AlarmScheduleType);
alarmScheduleType = AlarmScheduleType; alarmScheduleType = AlarmScheduleType;
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap; alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
dayOfWeekTranslationsArray = dayOfWeekTranslations; dayOfWeekTranslationsArray = dayOfWeekTranslations;

97
ui-ngx/src/app/modules/home/components/profile/alarm/create-alarm-rules.component.html

@ -16,49 +16,60 @@
--> -->
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<div *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index; @for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let isLast = $last) {
last as isLast;" class="flex flex-row items-center justify-start gap-2" <div class="flex flex-row items-center justify-start gap-2"
style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl"> style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl">
<div class="create-alarm-rule flex flex-1 flex-row items-stretch justify-start gap-2"> <div class="create-alarm-rule flex flex-1 flex-row items-stretch justify-start gap-2">
<mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker> <mat-form-field class="severity mat-block" floatLabel="always" hideRequiredMarker>
<mat-label translate>alarm.severity</mat-label> <mat-label translate>alarm.severity</mat-label>
<mat-select formControlName="severity" <mat-select formControlName="severity"
required required
placeholder="{{ 'device-profile.select-alarm-severity' | translate }}"> placeholder="{{ 'device-profile.select-alarm-severity' | translate }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity" @for (alarmSeverity of alarmSeverities; track alarmSeverity) {
[disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], $index)"> <mat-option [value]="alarmSeverity"
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }} [disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], $index)">
</mat-option> {{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-select> </mat-option>
<mat-error *ngIf="createAlarmRuleControl.get('severity').hasError('required')"> }
{{ 'device-profile.alarm-severity-required' | translate }} </mat-select>
</mat-error> @if (createAlarmRuleControl.get('severity').hasError('required')) {
</mat-form-field> <mat-error>
<mat-divider vertical></mat-divider> {{ 'device-profile.alarm-severity-required' | translate }}
<tb-alarm-rule formControlName="alarmRule" [deviceProfileId]="deviceProfileId" required class="flex-1"> </mat-error>
</tb-alarm-rule> }
</mat-form-field>
<mat-divider vertical></mat-divider>
<tb-alarm-rule formControlName="alarmRule" [deviceProfileId]="deviceProfileId" required class="flex-1">
</tb-alarm-rule>
</div>
@if (!disabled) {
<button
mat-icon-button color="primary" style="min-width: 40px;"
type="button"
(click)="removeCreateAlarmRule($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>remove_circle_outline</mat-icon>
</button>
}
</div> </div>
<button *ngIf="!disabled" }
mat-icon-button color="primary" style="min-width: 40px;" @if (!createAlarmRulesFormArray().controls.length && !disabled) {
type="button" <div>
(click)="removeCreateAlarmRule($index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<div *ngIf="!createAlarmRulesFormArray().controls.length && !disabled">
<span translate class="flex items-center justify-center" style="margin: 16px 0" <span translate class="flex items-center justify-center" style="margin: 16px 0"
class="tb-prompt required">device-profile.add-create-alarm-rule-prompt</span> class="tb-prompt required">device-profile.add-create-alarm-rule-prompt</span>
</div> </div>
<div *ngIf="!disabled"> }
<button mat-stroked-button color="primary" @if (!disabled) {
type="button" <div>
(click)="addCreateAlarmRule()" <button mat-stroked-button color="primary"
matTooltip="{{ 'device-profile.add-create-alarm-rule' | translate }}" type="button"
matTooltipPosition="above"> (click)="addCreateAlarmRule()"
<mat-icon class="button-icon">add_circle_outline</mat-icon> matTooltip="{{ 'device-profile.add-create-alarm-rule' | translate }}"
{{ 'device-profile.add-create-alarm-rule' | translate }} matTooltipPosition="above">
</button> <mat-icon class="button-icon">add_circle_outline</mat-icon>
</div> {{ 'device-profile.add-create-alarm-rule' | translate }}
</button>
</div>
}
</div> </div>

131
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarm.component.html

@ -24,13 +24,15 @@
</div> </div>
</mat-panel-title> </mat-panel-title>
<span class="flex-1"></span> <span class="flex-1"></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;" @if (!disabled) {
type="button" <button mat-icon-button style="min-width: 40px;"
(click)="removeAlarm.emit()" type="button"
matTooltip="{{ 'action.remove' | translate }}" (click)="removeAlarm.emit()"
matTooltipPosition="above"> matTooltip="{{ 'action.remove' | translate }}"
<mat-icon>delete</mat-icon> matTooltipPosition="above">
</button> <mat-icon>delete</mat-icon>
</button>
}
</div> </div>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
@ -39,15 +41,21 @@
<mat-form-field class="flex-1" floatLabel="always"> <mat-form-field class="flex-1" floatLabel="always">
<mat-label>{{'device-profile.alarm-type' | translate}}</mat-label> <mat-label>{{'device-profile.alarm-type' | translate}}</mat-label>
<input required matInput formControlName="alarmType" placeholder="Enter alarm type"> <input required matInput formControlName="alarmType" placeholder="Enter alarm type">
<mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')"> @if (alarmFormGroup.get('alarmType').hasError('required')) {
{{ 'device-profile.alarm-type-required' | translate }} <mat-error>
</mat-error> {{ 'device-profile.alarm-type-required' | translate }}
<mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('unique')"> </mat-error>
{{ 'device-profile.alarm-type-unique' | translate }} }
</mat-error> @if (alarmFormGroup.get('alarmType').hasError('unique')) {
<mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('maxlength')"> <mat-error>
{{ 'device-profile.alarm-type-max-length' | translate }} {{ 'device-profile.alarm-type-unique' | translate }}
</mat-error> </mat-error>
}
@if (alarmFormGroup.get('alarmType').hasError('maxlength')) {
<mat-error>
{{ 'device-profile.alarm-type-max-length' | translate }}
</mat-error>
}
</mat-form-field> </mat-form-field>
</div> </div>
<mat-expansion-panel class="advanced-settings" [expanded]="false"> <mat-expansion-panel class="advanced-settings" [expanded]="false">
@ -62,25 +70,28 @@
<mat-checkbox formControlName="propagate" style="display: block;"> <mat-checkbox formControlName="propagate" style="display: block;">
{{ 'device-profile.propagate-alarm' | translate }} {{ 'device-profile.propagate-alarm' | translate }}
</mat-checkbox> </mat-checkbox>
<section *ngIf="alarmFormGroup.get('propagate').value === true"> @if (alarmFormGroup.get('propagate').value === true) {
<mat-form-field floatLabel="always" class="mat-block"> <section>
<mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label> <mat-form-field floatLabel="always" class="mat-block">
<mat-chip-grid #relationTypesChipList [disabled]="disabled"> <mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
<mat-chip-row <mat-chip-grid #relationTypesChipList [disabled]="disabled">
*ngFor="let key of alarmFormGroup.get('propagateRelationTypes').value;" @for (key of alarmFormGroup.get('propagateRelationTypes').value; track key) {
(removed)="removeRelationType(key)"> <mat-chip-row
{{key}} (removed)="removeRelationType(key)">
<mat-icon matChipRemove>close</mat-icon> {{key}}
</mat-chip-row> <mat-icon matChipRemove>close</mat-icon>
<input matInput type="text" placeholder="{{'device-profile.alarm-rule-relation-types-list' | translate}}" </mat-chip-row>
[matChipInputFor]="relationTypesChipList" }
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" <input matInput type="text" placeholder="{{'device-profile.alarm-rule-relation-types-list' | translate}}"
matChipInputAddOnBlur [matChipInputFor]="relationTypesChipList"
(matChipInputTokenEnd)="addRelationType($event)"> [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
</mat-chip-grid> matChipInputAddOnBlur
<mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint> (matChipInputTokenEnd)="addRelationType($event)">
</mat-form-field> </mat-chip-grid>
</section> <mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
</section>
}
<mat-checkbox formControlName="propagateToOwner" style="display: block;"> <mat-checkbox formControlName="propagateToOwner" style="display: block;">
{{ 'device-profile.propagate-alarm-to-owner' | translate }} {{ 'device-profile.propagate-alarm-to-owner' | translate }}
</mat-checkbox> </mat-checkbox>
@ -103,29 +114,35 @@
<tb-alarm-rule formControlName="clearRule" class="flex-1" [deviceProfileId]="deviceProfileId"> <tb-alarm-rule formControlName="clearRule" class="flex-1" [deviceProfileId]="deviceProfileId">
</tb-alarm-rule> </tb-alarm-rule>
</div> </div>
<button *ngIf="!disabled" @if (!disabled) {
mat-icon-button color="primary" style="min-width: 40px;" <button
type="button" mat-icon-button color="primary" style="min-width: 40px;"
(click)="removeClearAlarmRule()" type="button"
matTooltip="{{ 'action.remove' | translate }}" (click)="removeClearAlarmRule()"
matTooltipPosition="above"> matTooltip="{{ 'action.remove' | translate }}"
<mat-icon>remove_circle_outline</mat-icon> matTooltipPosition="above">
</button> <mat-icon>remove_circle_outline</mat-icon>
</div> </button>
<div *ngIf="disabled && !alarmFormGroup.get('clearRule').value"> }
<span translate style="margin: 16px 0"
class="tb-prompt flex items-center justify-center">device-profile.no-clear-alarm-rule</span>
</div>
<div *ngIf="!disabled" [class.!hidden]="alarmFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
type="button"
(click)="addClearAlarmRule()"
matTooltip="{{ 'device-profile.add-clear-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
{{ 'device-profile.add-clear-alarm-rule' | translate }}
</button>
</div> </div>
@if (disabled && !alarmFormGroup.get('clearRule').value) {
<div>
<span translate style="margin: 16px 0"
class="tb-prompt flex items-center justify-center">device-profile.no-clear-alarm-rule</span>
</div>
}
@if (!disabled) {
<div [class.!hidden]="alarmFormGroup.get('clearRule').value">
<button mat-stroked-button color="primary"
type="button"
(click)="addClearAlarmRule()"
matTooltip="{{ 'device-profile.add-clear-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
{{ 'device-profile.add-clear-alarm-rule' | translate }}
</button>
</div>
}
</div> </div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>

43
ui-ngx/src/app/modules/home/components/profile/alarm/device-profile-alarms.component.html

@ -17,25 +17,30 @@
--> -->
<div class="flex flex-col"> <div class="flex flex-col">
<div class="tb-device-profile-alarms"> <div class="tb-device-profile-alarms">
<div *ngFor="let alarmControl of alarmsFormArray.controls; trackBy: trackByAlarm; @for (alarmControl of alarmsFormArray.controls; track trackByAlarm($index, alarmControl); let isLast = $last) {
let $index = index; last as isLast;" <div
class="flex flex-col" [class.pb-2]="!isLast"> class="flex flex-col" [class.pb-2]="!isLast">
<tb-device-profile-alarm [formControl]="alarmControl" <tb-device-profile-alarm [formControl]="alarmControl"
[expanded]="$index === 0" [expanded]="$index === 0"
[deviceProfileId]="deviceProfileId" [deviceProfileId]="deviceProfileId"
(removeAlarm)="removeAlarm($index)"> (removeAlarm)="removeAlarm($index)">
</tb-device-profile-alarm> </tb-device-profile-alarm>
</div> </div>
}
</div> </div>
<div *ngIf="!alarmsFormArray.controls.length"> @if (!alarmsFormArray.controls.length) {
<div>
<span translate <span translate
class="tb-prompt flex items-center justify-center">device-profile.no-alarm-rules</span> class="tb-prompt flex items-center justify-center">device-profile.no-alarm-rules</span>
</div> </div>
<div *ngIf="!disabled" style="padding-top: 16px;"> }
<button mat-raised-button color="primary" @if (!disabled) {
type="button" <div style="padding-top: 16px;">
(click)="addAlarm()"> <button mat-raised-button color="primary"
<span translate>device-profile.add-alarm-rule</span> type="button"
</button> (click)="addAlarm()">
</div> <span translate>device-profile.add-alarm-rule</span>
</button>
</div>
}
</div> </div>

20
ui-ngx/src/app/modules/home/components/profile/alarm/edit-alarm-details-dialog.component.html

@ -25,14 +25,16 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content> <div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async"> <fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<mat-form-field class="mat-block"> <mat-form-field class="mat-block">
<textarea matInput formControlName="alarmDetails" rows="5" <textarea matInput formControlName="alarmDetails" rows="5"
placeholder="{{ 'device-profile.alarm-rule-additional-info-placeholder' | translate }}"></textarea> placeholder="{{ 'device-profile.alarm-rule-additional-info-placeholder' | translate }}"></textarea>
<mat-hint [innerHTML]="'device-profile.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint> <mat-hint [innerHTML]="'device-profile.alarm-rule-additional-info-hint' | translate | safe: 'html'"></mat-hint>
</mat-form-field> </mat-form-field>
</div> </div>
@ -45,10 +47,12 @@
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }} {{ 'action.cancel' | translate }}
</button> </button>
<button *ngIf="!data.readonly" mat-raised-button color="primary" @if (!data.readonly) {
type="submit" <button mat-raised-button color="primary"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty"> type="submit"
{{ 'action.save' | translate }} [disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
</button> {{ 'action.save' | translate }}
</button>
}
</div> </div>
</form> </form>

107
ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html

@ -26,56 +26,73 @@
(keypress)="assetProfileEnter($event)" (keypress)="assetProfileEnter($event)"
[matAutocomplete]="assetProfileAutocomplete" [matAutocomplete]="assetProfileAutocomplete"
[class.!hidden]="showDetailsPageLink && useAssetProfileLink && disabled && selectAssetProfileFormGroup.get('assetProfile').value"> [class.!hidden]="showDetailsPageLink && useAssetProfileLink && disabled && selectAssetProfileFormGroup.get('assetProfile').value">
<a *ngIf="showDetailsPageLink && useAssetProfileLink && selectAssetProfileFormGroup.get('assetProfile').value && disabled" @if (showDetailsPageLink && useAssetProfileLink && selectAssetProfileFormGroup.get('assetProfile').value && disabled) {
aria-label="Open asset profile" [routerLink]=assetProfileURL> <a
{{ displayAssetProfileFn(selectAssetProfileFormGroup.get('assetProfile').value) }} aria-label="Open asset profile" [routerLink]=assetProfileURL>
</a> {{ displayAssetProfileFn(selectAssetProfileFormGroup.get('assetProfile').value) }}
<button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value && !disabled" </a>
type="button" }
matSuffix mat-icon-button aria-label="Clear" @if (selectAssetProfileFormGroup.get('assetProfile').value && !disabled) {
(click)="clear()"> <button
<mat-icon class="material-icons">close</mat-icon> type="button"
</button> matSuffix mat-icon-button aria-label="Clear"
<button *ngIf="selectAssetProfileFormGroup.get('assetProfile').value?.id && !disabled && editProfileEnabled" (click)="clear()">
type="button" <mat-icon class="material-icons">close</mat-icon>
matSuffix mat-icon-button aria-label="Edit" </button>
matTooltip="{{ 'asset-profile.edit' | translate }}" }
matTooltipPosition="above" @if (selectAssetProfileFormGroup.get('assetProfile').value?.id && !disabled && editProfileEnabled) {
(click)="editAssetProfile($event)"> <button
<mat-icon class="material-icons">edit</mat-icon> type="button"
</button> matSuffix mat-icon-button aria-label="Edit"
<button mat-button color="primary" matSuffix matTooltip="{{ 'asset-profile.edit' | translate }}"
(click)="createAssetProfile($event, '')" matTooltipPosition="above"
*ngIf="!selectAssetProfileFormGroup.get('assetProfile').value && !disabled && addNewProfile"> (click)="editAssetProfile($event)">
<span style="white-space: nowrap">{{ 'notification.create-new' | translate }}</span> <mat-icon class="material-icons">edit</mat-icon>
</button> </button>
}
@if (!selectAssetProfileFormGroup.get('assetProfile').value && !disabled && addNewProfile) {
<button mat-button color="primary" matSuffix
(click)="createAssetProfile($event, '')"
>
<span style="white-space: nowrap">{{ 'notification.create-new' | translate }}</span>
</button>
}
<mat-autocomplete <mat-autocomplete
class="tb-autocomplete" class="tb-autocomplete"
(closed)="onPanelClosed()" (closed)="onPanelClosed()"
#assetProfileAutocomplete="matAutocomplete" #assetProfileAutocomplete="matAutocomplete"
[displayWith]="displayAssetProfileFn"> [displayWith]="displayAssetProfileFn">
<mat-option *ngFor="let assetProfile of filteredAssetProfiles | async" [value]="assetProfile"> @for (assetProfile of filteredAssetProfiles | async; track assetProfile) {
<span [innerHTML]="assetProfile.name | highlight:searchText"></span> <mat-option [value]="assetProfile">
</mat-option> <span [innerHTML]="assetProfile.name | highlight:searchText"></span>
<mat-option *ngIf="!(filteredAssetProfiles | async)?.length" [value]="null" class="tb-not-found"> </mat-option>
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> }
<div *ngIf="!textIsNotEmpty(searchText) || !addNewProfile; else searchNotEmpty"> @if (!(filteredAssetProfiles | async)?.length) {
<span translate>asset-profile.no-asset-profiles-found</span> <mat-option [value]="null" class="tb-not-found">
</div> <div class="tb-not-found-content" (click)="$event.stopPropagation()">
<ng-template #searchNotEmpty> @if (!textIsNotEmpty(searchText) || !addNewProfile) {
<span> <div>
<span translate>asset-profile.no-asset-profiles-found</span>
</div>
} @else {
<span>
{{ translate.get('asset-profile.no-asset-profiles-matching', {{ translate.get('asset-profile.no-asset-profiles-matching',
{entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }} {entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span> </span>
<span> <span>
<a translate (click)="createAssetProfile($event, searchText)">asset-profile.create-new-asset-profile</a> <a translate (click)="createAssetProfile($event, searchText)">asset-profile.create-new-asset-profile</a>
</span> </span>
</ng-template> }
</div> </div>
</mat-option> </mat-option>
}
</mat-autocomplete> </mat-autocomplete>
<mat-error *ngIf="selectAssetProfileFormGroup.get('assetProfile').hasError('required')"> @if (selectAssetProfileFormGroup.get('assetProfile').hasError('required')) {
{{ 'asset-profile.asset-profile-required' | translate }} <mat-error>
</mat-error> {{ 'asset-profile.asset-profile-required' | translate }}
<mat-hint *ngIf="hint && !disabled">{{ hint | translate }}</mat-hint> </mat-error>
}
@if (hint && !disabled) {
<mat-hint>{{ hint | translate }}</mat-hint>
}
</mat-form-field> </mat-form-field>

9
ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html

@ -25,9 +25,12 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> </mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content> <div mat-dialog-content>
<tb-asset-profile <tb-asset-profile
#assetProfileComponent #assetProfileComponent

111
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html

@ -21,7 +21,9 @@
class="mat-block" class="mat-block"
[appearance]="inlineField ? 'outline' : appearance" [appearance]="inlineField ? 'outline' : appearance"
[subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing"> [subscriptSizing]="inlineField ? 'dynamic' : subscriptSizing">
<mat-label *ngIf="!inlineField">{{ 'device-profile.device-profile' | translate }}</mat-label> @if (!inlineField) {
<mat-label>{{ 'device-profile.device-profile' | translate }}</mat-label>
}
<input matInput type="text" <input matInput type="text"
#deviceProfileInput #deviceProfileInput
formControlName="deviceProfile" formControlName="deviceProfile"
@ -31,56 +33,73 @@
(keypress)="deviceProfileEnter($event)" (keypress)="deviceProfileEnter($event)"
[matAutocomplete]="deviceProfileAutocomplete" [matAutocomplete]="deviceProfileAutocomplete"
[class.!hidden]="showDetailsPageLink && useDeviceProfileLink && disabled && selectDeviceProfileFormGroup.get('deviceProfile').value"> [class.!hidden]="showDetailsPageLink && useDeviceProfileLink && disabled && selectDeviceProfileFormGroup.get('deviceProfile').value">
<a *ngIf="showDetailsPageLink && useDeviceProfileLink && selectDeviceProfileFormGroup.get('deviceProfile').value && disabled" @if (showDetailsPageLink && useDeviceProfileLink && selectDeviceProfileFormGroup.get('deviceProfile').value && disabled) {
aria-label="Open device profile" [routerLink]=deviceProfileURL> <a
{{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }} aria-label="Open device profile" [routerLink]=deviceProfileURL>
</a> {{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }}
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled" </a>
type="button" }
matSuffix mat-icon-button aria-label="Clear" @if (selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled) {
(click)="clear()"> <button
<mat-icon class="material-icons">close</mat-icon> type="button"
</button> matSuffix mat-icon-button aria-label="Clear"
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value?.id && !disabled && editProfileEnabled" (click)="clear()">
type="button" <mat-icon class="material-icons">close</mat-icon>
matSuffix mat-icon-button aria-label="Edit" </button>
matTooltip="{{ 'device-profile.edit' | translate }}" }
matTooltipPosition="above" @if (selectDeviceProfileFormGroup.get('deviceProfile').value?.id && !disabled && editProfileEnabled) {
(click)="editDeviceProfile($event)"> <button
<mat-icon class="material-icons">edit</mat-icon> type="button"
</button> matSuffix mat-icon-button aria-label="Edit"
<button mat-button color="primary" matSuffix matTooltip="{{ 'device-profile.edit' | translate }}"
(click)="createDeviceProfile($event, '')" matTooltipPosition="above"
*ngIf="!selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled && addNewProfile"> (click)="editDeviceProfile($event)">
<span style="white-space: nowrap">{{ 'notification.create-new' | translate }}</span> <mat-icon class="material-icons">edit</mat-icon>
</button> </button>
}
@if (!selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled && addNewProfile) {
<button mat-button color="primary" matSuffix
(click)="createDeviceProfile($event, '')"
>
<span style="white-space: nowrap">{{ 'notification.create-new' | translate }}</span>
</button>
}
<mat-autocomplete <mat-autocomplete
class="tb-autocomplete" class="tb-autocomplete"
(closed)="onPanelClosed()" (closed)="onPanelClosed()"
#deviceProfileAutocomplete="matAutocomplete" #deviceProfileAutocomplete="matAutocomplete"
[displayWith]="displayDeviceProfileFn"> [displayWith]="displayDeviceProfileFn">
<mat-option *ngFor="let deviceProfile of filteredDeviceProfiles | async" [value]="deviceProfile"> @for (deviceProfile of filteredDeviceProfiles | async; track deviceProfile) {
<span [innerHTML]="deviceProfile.name | highlight:searchText"></span> <mat-option [value]="deviceProfile">
</mat-option> <span [innerHTML]="deviceProfile.name | highlight:searchText"></span>
<mat-option *ngIf="!(filteredDeviceProfiles | async)?.length" [value]="null" class="tb-not-found"> </mat-option>
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> }
<div *ngIf="!textIsNotEmpty(searchText) || !addNewProfile; else searchNotEmpty"> @if (!(filteredDeviceProfiles | async)?.length) {
<span translate>device-profile.no-device-profiles-found</span> <mat-option [value]="null" class="tb-not-found">
</div> <div class="tb-not-found-content" (click)="$event.stopPropagation()">
<ng-template #searchNotEmpty> @if (!textIsNotEmpty(searchText) || !addNewProfile) {
<span> <div>
<span translate>device-profile.no-device-profiles-found</span>
</div>
} @else {
<span>
{{ translate.get('device-profile.no-device-profiles-matching', {{ translate.get('device-profile.no-device-profiles-matching',
{entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }} {entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span> </span>
<span> <span>
<a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a> <a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a>
</span> </span>
</ng-template> }
</div> </div>
</mat-option> </mat-option>
}
</mat-autocomplete> </mat-autocomplete>
<mat-error *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')"> @if (selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')) {
{{ 'device-profile.device-profile-required' | translate }} <mat-error>
</mat-error> {{ 'device-profile.device-profile-required' | translate }}
<mat-hint *ngIf="hint && !disabled">{{ hint | translate }}</mat-hint> </mat-error>
}
@if (hint && !disabled) {
<mat-hint>{{ hint | translate }}</mat-hint>
}
</mat-form-field> </mat-form-field>

9
ui-ngx/src/app/modules/home/components/profile/device-profile-dialog.component.html

@ -25,9 +25,12 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div> </mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content> <div mat-dialog-content>
<tb-device-profile <tb-device-profile
#deviceProfileComponent #deviceProfileComponent

32
ui-ngx/src/app/modules/home/components/profile/device/common/time-unit-select.component.html

@ -19,22 +19,30 @@
<mat-form-field class="mat-block flex-1" [appearance]="appearance"> <mat-form-field class="mat-block flex-1" [appearance]="appearance">
<mat-label>{{ labelText | translate }}</mat-label> <mat-label>{{ labelText | translate }}</mat-label>
<input matInput type="number" min="0" formControlName="time" required> <input matInput type="number" min="0" formControlName="time" required>
<mat-error *ngIf="timeUnitSelectFormGroup.get('time').hasError('required')"> @if (timeUnitSelectFormGroup.get('time').hasError('required')) {
{{ requiredText | translate }} <mat-error>
</mat-error> {{ requiredText | translate }}
<mat-error *ngIf="timeUnitSelectFormGroup.get('time').hasError('pattern')"> </mat-error>
{{ patternText | translate }} }
</mat-error> @if (timeUnitSelectFormGroup.get('time').hasError('pattern')) {
<mat-error *ngIf="timeUnitSelectFormGroup.get('time').hasError('min')"> <mat-error>
{{ (minText || patternText) | translate : {min: minTime/1000} }} {{ patternText | translate }}
</mat-error> </mat-error>
}
@if (timeUnitSelectFormGroup.get('time').hasError('min')) {
<mat-error>
{{ (minText || patternText) | translate : {min: minTime/1000} }}
</mat-error>
}
</mat-form-field> </mat-form-field>
<mat-form-field class="mat-block flex-1" [appearance]="appearance"> <mat-form-field class="mat-block flex-1" [appearance]="appearance">
<mat-label translate>device-profile.condition-duration-time-unit</mat-label> <mat-label translate>device-profile.condition-duration-time-unit</mat-label>
<mat-select formControlName="unit"> <mat-select formControlName="unit">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit"> @for (timeUnit of timeUnits; track timeUnit) {
{{ timeUnitTranslations.get(timeUnit) | translate }} <mat-option [value]="timeUnit">
</mat-option> {{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
}
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</section> </section>

14
ui-ngx/src/app/modules/home/components/profile/device/device-profile-configuration.component.html

@ -16,11 +16,13 @@
--> -->
<div [formGroup]="deviceProfileConfigurationFormGroup"> <div [formGroup]="deviceProfileConfigurationFormGroup">
<div [ngSwitch]="type"> <div>
<ng-template [ngSwitchCase]="deviceProfileType.DEFAULT"> @switch (type) {
<tb-default-device-profile-configuration @case (deviceProfileType.DEFAULT) {
formControlName="configuration"> <tb-default-device-profile-configuration
</tb-default-device-profile-configuration> formControlName="configuration">
</ng-template> </tb-default-device-profile-configuration>
}
}
</div> </div>
</div> </div>

56
ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html

@ -16,32 +16,34 @@
--> -->
<div [formGroup]="deviceProfileTransportConfigurationFormGroup"> <div [formGroup]="deviceProfileTransportConfigurationFormGroup">
<div [ngSwitch]="transportType"> <div>
<ng-template [ngSwitchCase]="deviceTransportType.DEFAULT"> @switch (transportType) {
<tb-default-device-profile-transport-configuration @case (deviceTransportType.DEFAULT) {
formControlName="configuration"> <tb-default-device-profile-transport-configuration
</tb-default-device-profile-transport-configuration> formControlName="configuration">
</ng-template> </tb-default-device-profile-transport-configuration>
<ng-template [ngSwitchCase]="deviceTransportType.MQTT"> }
<tb-mqtt-device-profile-transport-configuration @case (deviceTransportType.MQTT) {
formControlName="configuration"> <tb-mqtt-device-profile-transport-configuration
</tb-mqtt-device-profile-transport-configuration> formControlName="configuration">
</ng-template> </tb-mqtt-device-profile-transport-configuration>
<ng-template [ngSwitchCase]="deviceTransportType.COAP"> }
<tb-coap-device-profile-transport-configuration @case (deviceTransportType.COAP) {
formControlName="configuration"> <tb-coap-device-profile-transport-configuration
</tb-coap-device-profile-transport-configuration> formControlName="configuration">
</ng-template> </tb-coap-device-profile-transport-configuration>
<ng-template [ngSwitchCase]="deviceTransportType.LWM2M"> }
<tb-profile-lwm2m-device-transport-configuration @case (deviceTransportType.LWM2M) {
[isAdd]="isAdd" <tb-profile-lwm2m-device-transport-configuration
formControlName="configuration"> [isAdd]="isAdd"
</tb-profile-lwm2m-device-transport-configuration> formControlName="configuration">
</ng-template> </tb-profile-lwm2m-device-transport-configuration>
<ng-template [ngSwitchCase]="deviceTransportType.SNMP"> }
<tb-snmp-device-profile-transport-configuration @case (deviceTransportType.SNMP) {
formControlName="configuration"> <tb-snmp-device-profile-transport-configuration
</tb-snmp-device-profile-transport-configuration> formControlName="configuration">
</ng-template> </tb-snmp-device-profile-transport-configuration>
}
}
</div> </div>
</div> </div>

19
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-attributes-dialog.component.html

@ -27,8 +27,10 @@
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
</mat-toolbar> </mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async"> @if (isLoading$ | async) {
</mat-progress-bar> <mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content> <div mat-dialog-content>
<tb-lwm2m-attributes-key-list <tb-lwm2m-attributes-key-list
[isResource]="isResource" [isResource]="isResource"
@ -42,11 +44,12 @@
(click)="cancel()" cdkFocusInitial> (click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }} {{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button> </button>
<button mat-raised-button color="primary" @if (!readonly) {
*ngIf="!readonly" <button mat-raised-button color="primary"
type="submit" type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty"> [disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
{{ 'action.save' | translate }} {{ 'action.save' | translate }}
</button> </button>
}
</div> </div>
</form> </form>

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

Loading…
Cancel
Save