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

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

@ -26,25 +26,31 @@
</mat-toolbar>
<div mat-dialog-content>
<div class="flex h-full flex-1 flex-col items-center justify-center">
<mat-progress-spinner color="warn" mode="indeterminate"
strokeWidth="5" *ngIf="!showCheckSuccess && !checkErrMsg">
</mat-progress-spinner>
<div *ngIf="showCheckSuccess">
<div class="flex flex-1 items-center justify-center">
<mat-icon class="success connection-icon">check_circle</mat-icon>
@if (!showCheckSuccess && !checkErrMsg) {
<mat-progress-spinner color="warn" mode="indeterminate"
strokeWidth="5">
</mat-progress-spinner>
}
@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>
<span class="connection-status success">{{ "ai-models.check-connectivity-success" | translate }}</span>
</div>
<div *ngIf="checkErrMsg" class="w-full">
<div class="flex flex-1 items-center justify-center">
<mat-icon class="error connection-icon">cancel</mat-icon>
}
@if (checkErrMsg) {
<div class="w-full">
<div class="flex flex-1 items-center justify-center">
<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 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 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>
</button>
</div>
<div *ngIf="!configFormGroup.get('clearRule').value">
<span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span>
</div>
@if (!configFormGroup.get('clearRule').value) {
<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">
<button mat-stroked-button color="primary"
[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.
-->
<ng-container *ngIf="panelMode; else componentMode">
@if (panelMode) {
<ng-container *ngTemplateOutlet="alarmRuleFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else alarmFilter">
} @else {
@if (buttonMode) {
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
@ -27,8 +26,47 @@
(click)="toggleAlarmRuleFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
} @else {
<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>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="alarmFilter"></ng-container>
@ -70,9 +108,11 @@
<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>
<mat-option *ngFor="let type of listEntityTypes" [value]="type">
{{ entityTypeTranslations.get(type)?.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>

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

@ -15,26 +15,28 @@
limitations under the License.
-->
<div class="tb-details-buttons xs:flex xs:flex-col" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{ 'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{ 'action.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{ 'action.delete' | translate }}
</button>
</div>
@if (!standalone) {
<div class="tb-details-buttons xs:flex xs:flex-col">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{ 'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{ 'action.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{ 'action.delete' | translate }}
</button>
</div>
}
<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-gap">
@ -55,11 +57,11 @@
<mat-hint></mat-hint>
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
formControlName="debugSettings"
style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
<tb-entity-select formControlName="entityId"
appearance="outline"
@ -107,9 +109,11 @@
</button>
}
</div>
<div *ngIf="!configFormGroup.get('clearRule').value">
<span translate class="tb-prompt flex items-center justify-center text-base">alarm-rule.no-clear-alarm-rule</span>
</div>
@if (!configFormGroup.get('clearRule').value) {
<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">
<button mat-stroked-button color="primary"
[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>
</div>
}
</div>
</div>
</div>
<section class="tb-form-panel stroked">
<div class="flex flex-row items-center justify-between">
<div class="tb-form-panel-title">{{ 'alarm-rule.type' | translate }}</div>
<tb-toggle-select formControlName="type" selectMediaBreakpoint="xs">
<tb-toggle-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</tb-toggle-option>
@for (alarmConditionType of alarmConditionTypes; track alarmConditionType) {
<tb-toggle-option [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</tb-toggle-option>
}
</tb-toggle-select>
</div>
@if (isNoData) {
@ -157,13 +159,17 @@
<mat-form-field class="mat-block" appearance="outline">
<mat-select formControlName="unit" required
placeholder="{{ 'alarm-rule.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
@for (timeUnit of timeUnits; track timeUnit) {
<mat-option [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
}
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('unit').hasError('required')">
{{ 'alarm-rule.condition-duration-time-unit-required' | translate }}
</mat-error>
@if (conditionFormGroup.get('unit').hasError('required')) {
<mat-error>
{{ 'alarm-rule.condition-duration-time-unit-required' | translate }}
</mat-error>
}
</mat-form-field>
</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) {
<section>
<mat-chip-listbox multiple formControlName="daysOfWeek">
<mat-chip-option *ngFor="let day of allDays; let i = index;"
[value]="i+1">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-chip-option>
@for (day of allDays; track day; let i = $index) {
<mat-chip-option
[value]="i+1">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-chip-option>
}
</mat-chip-listbox>
<tb-error class="mb-2 block" [error]="alarmScheduleForm.get('staticValue.daysOfWeek').hasError('required')
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
@ -71,30 +73,32 @@
}
@if (alarmScheduleForm.get('staticValue.type').value === alarmScheduleType.CUSTOM) {
<section class="flex flex-col gap-4">
<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-chip-listbox formControlName="enabled">
<mat-chip-option class="w-32" color="primary" (selectionChange)="changeCustomScheduler($event, day)" [value]="true">{{ dayOfWeekTranslationsArray[day] | translate }}</mat-chip-option>
</mat-chip-listbox>
<div class="flex flex-1 flex-row gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.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="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.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 text-center sm:max-w-[120px]"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
@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-chip-listbox formControlName="enabled">
<mat-chip-option class="w-32" color="primary" (selectionChange)="changeCustomScheduler($event, day)" [value]="true">{{ dayOfWeekTranslationsArray[day] | translate }}</mat-chip-option>
</mat-chip-listbox>
<div class="flex flex-1 flex-row gap-2">
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.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="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-label translate>alarm-rule.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 text-center sm:max-w-[120px]"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div>
</div>
</div>
</div>
}
<tb-error class="block" [error]="alarmScheduleForm.get('staticValue.items').hasError('dayOfWeeks')
? ('alarm-rule.schedule-days-of-week-required' | translate) : ''"></tb-error>
</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"
required
placeholder="{{ 'alarm-rule.select-alarm-severity' | translate }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"
[disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], index)">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
@for (alarmSeverity of alarmSeverities; track alarmSeverity) {
<mat-option [value]="alarmSeverity"
[disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], index)">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<tb-cf-alarm-rule formControlName="alarmRule" [arguments]="arguments" [testScript]="testScript" required class="flex-1">
</tb-cf-alarm-rule>
</div>
<button *ngIf="!disabled"
class="button-icon"
mat-icon-button
type="button"
(click)="removeCreateAlarmRule(index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
@if (!disabled) {
<button
class="button-icon"
mat-icon-button
type="button"
(click)="removeCreateAlarmRule(index)"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
}
</div>
}
@if (!createAlarmRulesFormArray().controls.length) {
<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>
}
<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>
<span>{{ entityKeyValueTypes.get(filterFormGroup.get('valueType').value)?.name | translate }}</span>
</mat-select-trigger>
<mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType">
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
</mat-option>
@for (valueType of entityKeyValueTypesKeys; track valueType) {
<mat-option [value]="valueType">
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
</mat-option>
}
</mat-select>
<mat-error *ngIf="filterFormGroup.get('valueType').hasError('required')">
{{ 'filter.value-type-required' | translate }}
</mat-error>
@if (filterFormGroup.get('valueType').hasError('required')) {
<mat-error>
{{ 'filter.value-type-required' | translate }}
</mat-error>
}
</mat-form-field>
</div>
</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">
<input required type="number" matInput formControlName="staticValue" min="1"
placeholder="{{'filter.value' | translate}}">
@if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.staticValue').hasError('required') ? 'alarm-rule.value-required' : 'alarm-rule.min-value') | translate"
class="tb-error">
warning
</mat-icon>
}
@if (filterPredicateValueNoDataFormGroup.get('duration.staticValue').touched && filterPredicateValueNoDataFormGroup.get('duration.staticValue').errors) {
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(filterPredicateValueNoDataFormGroup.get('duration.staticValue').hasError('required') ? 'alarm-rule.value-required' : 'alarm-rule.min-value') | translate"
class="tb-error">
warning
</mat-icon>
}
</mat-form-field>
} @else {
<mat-form-field class="w-full flex-1" appearance="outline" subscriptSizing="dynamic">
@ -68,9 +68,11 @@
</ng-container>
<mat-form-field class="mat-block flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="unit" required placeholder="{{'alarm-rule.time-unit' | translate}}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitsTranslationMap.get(timeUnit) | translate }}
</mat-option>
@for (timeUnit of timeUnits; track timeUnit) {
<mat-option [value]="timeUnit">
{{ timeUnitsTranslationMap.get(timeUnit) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
</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">
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of stringOperations" [value]="operation">
{{stringOperationTranslationMap.get(stringOperation[operation]) | translate}}
</mat-option>
@for (operation of stringOperations; track operation) {
<mat-option [value]="operation">
{{stringOperationTranslationMap.get(stringOperation[operation]) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
@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%">
<mat-form-field hideRequiredMarker class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of numericOperations" [value]="operation">
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
</mat-option>
@for (operation of numericOperations; track operation) {
<mat-option [value]="operation">
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@ -49,9 +53,11 @@
<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-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of booleanOperations" [value]="operation">
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
</mat-option>
@for (operation of booleanOperations; track operation) {
<mat-option [value]="operation">
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>

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

@ -26,32 +26,42 @@
#userAutocomplete="matAutocomplete"
[displayWith]="displayUserFn"
(optionSelected)="selected($event)">
<mat-option *ngIf="displayAssigneeNotSet" [value]="assigneeOptions.noAssignee">
<mat-icon class="unassigned-icon">account_circle</mat-icon>
<span>{{ assigneeNotSetText | translate }}</span>
</mat-option>
<mat-option *ngIf="displayAssignedToCurrentUser" [value]="assigneeOptions.currentUser">
<mat-icon class="unassigned-icon">account_circle</mat-icon>
<span>{{ assignedToCurrentUserText | translate }}</span>
</mat-option>
<mat-option *ngFor="let user of filteredUsers | async" [value]="user"
[class.!hidden]="assigneeId === user.id.id">
<span class="user-avatar" [innerHTML]="getUserInitials(user)"
[style.background-color]="getAvatarBgColor(user)">
</span>
<div class="user-display-name flex flex-col gap-0.5">
<span *ngIf="user.firstName || user.lastName"
[innerHTML]="getFullName(user) | highlight:searchText"></span>
<span [innerHTML]="user.email | highlight:searchText"></span>
</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 }}
@if (displayAssigneeNotSet) {
<mat-option [value]="assigneeOptions.noAssignee">
<mat-icon class="unassigned-icon">account_circle</mat-icon>
<span>{{ assigneeNotSetText | translate }}</span>
</mat-option>
}
@if (displayAssignedToCurrentUser) {
<mat-option [value]="assigneeOptions.currentUser">
<mat-icon class="unassigned-icon">account_circle</mat-icon>
<span>{{ assignedToCurrentUserText | translate }}</span>
</mat-option>
}
@for (user of filteredUsers | async; track user) {
<mat-option [value]="user"
[class.!hidden]="assigneeId === user.id.id">
<span class="user-avatar" [innerHTML]="getUserInitials(user)"
[style.background-color]="getAvatarBgColor(user)">
</span>
</div>
</mat-option>
<div class="user-display-name flex flex-col gap-0.5">
@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-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'}"
(click)="openAlarmAssigneeSelectPanel($event)"
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">
<span *ngIf="assignee" matPrefix class="user-avatar" [class.inline-icon]="inline"
[style.background-color]="getAvatarBgColor()">
{{ getUserInitials() }}
</span>
<mat-icon *ngIf="!assignee" matPrefix class="unassigned-icon" [class.inline-icon]="inline">account_circle</mat-icon>
<mat-icon *ngIf="!disabled" matSuffix>arrow_drop_down</mat-icon>
@if (assignee) {
<span matPrefix class="user-avatar" [class.inline-icon]="inline"
[style.background-color]="getAvatarBgColor()">
{{ getUserInitials() }}
</span>
}
@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>
</ng-container>

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

@ -20,12 +20,13 @@
subscriptSizing="dynamic">
<mat-label translate>alarm.assignee</mat-label>
<input matInput readonly [value]="getAssignee()">
<span *ngIf="userAssigned; else unassigned" matPrefix class="user-avatar"
[style.background-color]="getAvatarBgColor(alarm.assignee)">
{{ getUserInitials(alarm.assignee) }}
</span>
<ng-template #unassigned>
@if (userAssigned) {
<span matPrefix class="user-avatar"
[style.background-color]="getAvatarBgColor(alarm.assignee)">
{{ getUserInitials(alarm.assignee) }}
</span>
} @else {
<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-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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content style="padding: 0">
<tb-alarm-comment [alarmId]="alarmId"
[alarmActivityOnly]="true">

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

@ -49,104 +49,110 @@
</div>
</div>
<div class="flex flex-col">
<ng-container *ngIf="!isDirectionAscending()">
@if (!isDirectionAscending()) {
<ng-container *ngTemplateOutlet="commentInput"></ng-container>
</ng-container>
}
<div class="comments-container flex flex-col gap-3" style="padding: 24px"
[class.asc]="isDirectionAscending()"
[class.activity-only]="alarmActivityOnly">
<div class="flex-1" *ngFor="let displayDataElement of displayData; index as i">
<div class="system-comment" *ngIf="displayDataElement.isSystemComment; else userComment">
<span class="system-text" style="margin-right: 8px">
{{ displayDataElement.commentText }}
</span>
<span class="time" style="padding: 3px"
[matTooltip]="displayDataElement.createdTime"
matTooltipPosition="right">
{{ displayDataElement.createdDateAgo }}
</span>
</div>
<ng-template #userComment>
<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>
@for (displayDataElement of displayData; track displayDataElement; let i = $index) {
<div class="flex-1">
@if (displayDataElement.isSystemComment) {
<div class="system-comment">
<span class="system-text" style="margin-right: 8px">
{{ displayDataElement.commentText }}
</span>
<span class="time" style="padding: 3px"
[matTooltip]="displayDataElement.createdTime"
matTooltipPosition="right">
{{ displayDataElement.createdDateAgo }}
</span>
</div>
</div>
<ng-template #commentEditing>
<div class="flex flex-row items-center justify-start gap-2">
<div class="user-avatar flex flex-row items-center justify-center"
[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">
} @else {
@if (!displayDataElement.edit) {
<div class="user-comment flex flex-row items-center justify-start gap-2"
(mouseenter)="onCommentMouseEnter(displayDataElement.commentId, i)"
(mouseleave)="onCommentMouseLeave(i)">
@if (displayDataElement.userExists) {
<div
class="user-avatar flex flex-row items-center justify-center self-start xs:!hidden"
[style.background-color]="displayDataElement.avatarBgColor">
{{ getUserInitials(displayDataElement.displayName) }}
</div>
} @else {
<mat-icon class="mat-icon tb-mat-28 xs:!hidden">no_accounts</mat-icon>
}
<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>
@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
(click)="cancelEdit(displayDataElement.commentId)"
type="button">
<mat-icon class="material-icons red-button">close</mat-icon>
type="button"
(click)="editComment(displayDataElement.commentId)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button mat-icon-button
type="button"
(click)="saveEditedComment(displayDataElement.commentId)">
<mat-icon class="material-icons green-button">check</mat-icon>
(click)="deleteComment(displayDataElement.commentId)">
<mat-icon class="material-icons">delete</mat-icon>
</button>
</div>
</mat-form-field>
</div>
</ng-template>
</ng-template>
</div>
</div>
} @else {
<div class="flex flex-row items-center justify-start gap-2">
<div class="user-avatar flex flex-row items-center justify-center"
[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>
<ng-container *ngIf="isDirectionAscending()">
@if (isDirectionAscending()) {
<ng-container *ngTemplateOutlet="commentInput"></ng-container>
</ng-container>
}
</div>
<ng-template #commentInput>
<div style="background-color: white" class="comment-input"
@ -154,11 +160,12 @@
[class.oldest-first]="isDirectionAscending()"
[class.activity-only]="alarmActivityOnly">
<div class="inner-wrap flex flex-row items-center justify-start gap-2">
<div class="user-avatar flex flex-row items-center justify-center"
*ngIf="userDisplayName$ | async; let userDisplayName"
[style.background-color]="getCurrentUserBgColor(userDisplayName)">
{{ getUserInitials(userDisplayName) }}
</div>
@if (userDisplayName$ | async; as userDisplayName) {
<div class="user-avatar flex flex-row items-center justify-center"
[style.background-color]="getCurrentUserBgColor(userDisplayName)">
{{ getUserInitials(userDisplayName) }}
</div>
}
<mat-form-field class="mat-block tb-appearance-transparent flex-1">
<textarea matInput
type="text"
@ -170,15 +177,16 @@
(keyup.enter)="saveComment()"
(keydown.enter)="$event.preventDefault()">
</textarea>
<button mat-icon-button
type="button"
matSuffix
*ngIf="getAlarmCommentValue()"
(click)="saveComment()">
<mat-icon color="primary">
send
</mat-icon>
</button>
@if (getAlarmCommentValue()) {
<button mat-icon-button
type="button"
matSuffix
(click)="saveComment()">
<mat-icon color="primary">
send
</mat-icon>
</button>
}
</mat-form-field>
</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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
@if ((isLoading$ | async) === false) {
<div style="height: 4px;"></div>
}
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" style="margin-bottom: 22px">
<div class="flex flex-row gap-1.5">
@ -43,14 +47,18 @@
</mat-form-field>
</div>
<div class="flex flex-row gap-1.5">
<mat-form-field *ngIf="alarmFormGroup.get('startTime').value" class="mat-block flex-1">
<mat-label translate>alarm.start-time</mat-label>
<input matInput formControlName="startTime" readonly>
</mat-form-field>
<mat-form-field *ngIf="alarmFormGroup.get('duration').value" class="mat-block flex-1">
<mat-label translate>alarm.duration</mat-label>
<input matInput formControlName="duration" readonly>
</mat-form-field>
@if (alarmFormGroup.get('startTime').value) {
<mat-form-field class="mat-block flex-1">
<mat-label translate>alarm.start-time</mat-label>
<input matInput formControlName="startTime" readonly>
</mat-form-field>
}
@if (alarmFormGroup.get('duration').value) {
<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 class="flex flex-row gap-1.5">
<mat-form-field class="mat-block flex-1">
@ -71,12 +79,13 @@
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-json-object-edit
*ngIf="displayDetails"
formControlName="alarmDetails"
readonly
label="{{ 'alarm.additional-info' | translate }}">
</tb-json-object-edit>
@if (displayDetails) {
<tb-json-object-edit
formControlName="alarmDetails"
readonly
label="{{ 'alarm.additional-info' | translate }}">
</tb-json-object-edit>
}
</ng-template>
</mat-expansion-panel>
</fieldset>
@ -88,25 +97,31 @@
</div>
<div mat-dialog-actions class="flex flex-row">
<span class="flex-1"></span>
<div class="flex flex-row gap-2" *ngIf="alarm$ | async; let alarm;">
<button *ngIf="allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK ||
alarm.status === alarmStatuses.CLEARED_UNACK)"
mat-raised-button
color="primary"
type="button"
(click)="acknowledge()"
[disabled]="(isLoading$ | async)">
{{ 'alarm.acknowledge' | translate }}
</button>
<button *ngIf="allowClear && (alarm.status === alarmStatuses.ACTIVE_ACK ||
alarm.status === alarmStatuses.ACTIVE_UNACK)"
mat-raised-button
color="primary"
type="button"
(click)="clear()"
[disabled]="(isLoading$ | async)">
{{ 'alarm.clear' | translate }}
</button>
</div>
@if (alarm$ | async; as alarm;) {
<div class="flex flex-row gap-2">
@if (allowAcknowledgment && (alarm.status === alarmStatuses.ACTIVE_UNACK ||
alarm.status === alarmStatuses.CLEARED_UNACK)) {
<button
mat-raised-button
color="primary"
type="button"
(click)="acknowledge()"
[disabled]="(isLoading$ | async)">
{{ 'alarm.acknowledge' | translate }}
</button>
}
@if (allowClear && (alarm.status === alarmStatuses.ACTIVE_ACK ||
alarm.status === alarmStatuses.ACTIVE_UNACK)) {
<button
mat-raised-button
color="primary"
type="button"
(click)="clear()"
[disabled]="(isLoading$ | async)">
{{ 'alarm.clear' | translate }}
</button>
}
</div>
}
</div>
</form>

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

@ -15,11 +15,10 @@
limitations under the License.
-->
<ng-container *ngIf="panelMode; else componentMode">
@if (panelMode) {
<ng-container *ngTemplateOutlet="alarmFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else alarmFilter">
} @else {
@if (buttonMode) {
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
@ -27,8 +26,52 @@
(click)="toggleAlarmFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
} @else {
<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>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="alarmFilter"></ng-container>
@ -59,17 +102,21 @@
<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">
<mat-chip-option *ngFor="let searchStatus of alarmSearchStatuses" [value]="searchStatus">
{{ alarmSearchStatusTranslationMap.get(searchStatus) | translate }}
</mat-chip-option>
@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">
<mat-chip-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-chip-option>
@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">
@ -85,11 +132,13 @@
<div class="fixed-title-width" translate>alarm.assignee</div>
<tb-alarm-assignee-select class="flex-1" inline="true"
[userMode]="userMode"
formControlName="assigneeId">
formControlName="assigneeId">
</tb-alarm-assignee-select>
</div>
<mat-slide-toggle class="mat-slide" *ngIf="propagatedFilter" formControlName="searchPropagatedAlarms">
{{ 'alarm.search-propagated-alarms' | translate }}
</mat-slide-toggle>
@if (propagatedFilter) {
<mat-slide-toggle class="mat-slide" formControlName="searchPropagatedAlarms">
{{ 'alarm.search-propagated-alarms' | translate }}
</mat-slide-toggle>
}
</div>
</ng-template>

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

@ -22,23 +22,29 @@
formControlName="entityInfo"
[required]="required"
[matAutocomplete]="entityInfoAutocomplete">
<button *ngIf="selectEntityInfoFormGroup.get('entityInfo').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
@if (selectEntityInfoFormGroup.get('entityInfo').value && !disabled) {
<button
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
}
<mat-autocomplete
class="tb-autocomplete"
#entityInfoAutocomplete="matAutocomplete"
[displayWith]="displayEntityInfoFn">
<mat-option *ngFor="let entityInfo of filteredEntityInfos | async" [value]="entityInfo">
<span [innerHTML]="entityInfo.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntityInfos | async)?.length" [value]="null">
<span>
@for (entityInfo of filteredEntityInfos | async; track entityInfo) {
<mat-option [value]="entityInfo">
<span [innerHTML]="entityInfo.name | highlight:searchText"></span>
</mat-option>
}
@if (!(filteredEntityInfos | async)?.length) {
<mat-option [value]="null">
<span>
{{ translate.get('entity.no-entities-matching', {entity: searchText}) | async }}
</span>
</mat-option>
</span>
</mat-option>
}
</mat-autocomplete>
</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="flex flex-row" *ngFor="let alias of entityAliasesInfo | keyvalue">
<tb-aliases-entity-autocomplete class="flex-1"
required
[alias]="alias.value.alias"
[entityFilter]="alias.value.entityFilter"
[(ngModel)]="alias.value.currentEntity"
(ngModelChange)="currentAliasEntityChanged(alias.key, alias.value.currentEntity)">
</tb-aliases-entity-autocomplete>
</div>
@for (alias of entityAliasesInfo | keyvalue; track alias) {
<div class="flex flex-row">
<tb-aliases-entity-autocomplete class="flex-1"
required
[alias]="alias.value.alias"
[entityFilter]="alias.value.entityFilter"
[(ngModel)]="alias.value.currentEntity"
(ngModelChange)="currentAliasEntityChanged(alias.key, alias.value.currentEntity)">
</tb-aliases-entity-autocomplete>
</div>
}
</div>

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

@ -15,18 +15,20 @@
limitations under the License.
-->
<section class="tb-aliases-entity-select flex flex-row items-center justify-start" *ngIf="hasSelectableAliasEntities">
<button mat-icon-button
cdkOverlayOrigin #aliasEntitySelectPanelOrigin="cdkOverlayOrigin"
@if (hasSelectableAliasEntities) {
<section class="tb-aliases-entity-select flex flex-row items-center justify-start">
<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()"
matTooltip="{{ 'entity.select-entities' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon class="material-icons">devices_other</mat-icon>
</button>
<span class="lt-lg:!hidden"
(click)="openEditMode()"
matTooltip="{{ 'entity.select-entities' | translate }}"
[matTooltipPosition]="tooltipPosition">
{{displayValue}}
</span>
</section>
{{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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content>
<fieldset class="tb-form-panel outlined" [disabled]="isLoading$ | async">
<div class="flex flex-row items-start xs:flex-col xs:items-stretch">
<mat-form-field class="flex-1" appearance="outline">
<mat-label translate>alias.name</mat-label>
<input matInput formControlName="alias" required>
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('required')">
{{ 'alias.name-required' | translate }}
</mat-error>
<mat-error *ngIf="entityAliasFormGroup.get('alias').hasError('duplicateAliasName')">
{{ 'alias.duplicate-alias' | translate }}
</mat-error>
@if (entityAliasFormGroup.get('alias').hasError('required')) {
<mat-error>
{{ 'alias.name-required' | translate }}
</mat-error>
}
@if (entityAliasFormGroup.get('alias').hasError('duplicateAliasName')) {
<mat-error>
{{ 'alias.duplicate-alias' | translate }}
</mat-error>
}
</mat-form-field>
<div class="tb-resolve-multiple-switch" tb-hint-tooltip-icon="{{ 'alias.resolve-multiple-hint' | translate }}">
<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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
@if ((isLoading$ | async) === false) {
<div style="height: 4px;"></div>
}
<div mat-dialog-content>
<div class="tb-aliases-header flex flex-row items-center justify-start">
<span class="max-w-5% flex-full"></span>
@ -40,45 +44,49 @@
</div>
<fieldset [disabled]="isLoading$ | async" class="tb-form-panel outlined">
<mat-divider></mat-divider>
<div class="flex flex-1 flex-row items-center justify-start"
formArrayName="entityAliases"
*ngFor="let entityAliasControl of entityAliasesFormArray().controls; let $index = index">
<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">
<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">
<input matInput [formControl]="entityAliasControl.get('alias')" required placeholder="{{ 'entity.alias' | translate }}">
<mat-error *ngIf="entityAliasControl.get('alias').hasError('required')">
{{ 'entity.alias-required' | translate }}
</mat-error>
</mat-form-field>
<tb-entity-filter-view class="flex-1" style="padding-left: 10px;" [formControl]="entityAliasControl.get('filter')">
</tb-entity-filter-view>
<section style="padding-left: 10px;"
class="tb-resolve-multiple-switch flex min-w-32 max-w-32 flex-1 basis-32 flex-col items-center justify-center">
<mat-slide-toggle class="resolve-multiple-switch"
[formControl]="entityAliasControl.get('resolveMultiple')">
</mat-slide-toggle>
</section>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="editAlias($index)"
matTooltip="{{ 'alias.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="removeAlias($index)"
matTooltip="{{ 'entity.remove-alias' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
@for (entityAliasControl of entityAliasesFormArray().controls; track entityAliasControl) {
<div class="flex flex-1 flex-row items-center justify-start"
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">
<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">
<input matInput [formControl]="entityAliasControl.get('alias')" required placeholder="{{ 'entity.alias' | translate }}">
@if (entityAliasControl.get('alias').hasError('required')) {
<mat-error>
{{ 'entity.alias-required' | translate }}
</mat-error>
}
</mat-form-field>
<tb-entity-filter-view class="flex-1" style="padding-left: 10px;" [formControl]="entityAliasControl.get('filter')">
</tb-entity-filter-view>
<section style="padding-left: 10px;"
class="tb-resolve-multiple-switch flex min-w-32 max-w-32 flex-1 basis-32 flex-col items-center justify-center">
<mat-slide-toggle class="resolve-multiple-switch"
[formControl]="entityAliasControl.get('resolveMultiple')">
</mat-slide-toggle>
</section>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="editAlias($index)"
matTooltip="{{ 'alias.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="removeAlias($index)"
matTooltip="{{ 'entity.remove-alias' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
}
</fieldset>
</div>
<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-label translate>api-key.description</mat-label>
<textarea cdkTextareaAutosize matInput formControlName="description" rows="2" maxLength="255"></textarea>
<mat-error *ngIf="apiKeyForm.get('description').hasError('required')">
{{ 'asset.description-required' | translate }}
</mat-error>
@if (apiKeyForm.get('description').hasError('required')) {
<mat-error>
{{ 'asset.description-required' | translate }}
</mat-error>
}
</mat-form-field>
<mat-slide-toggle class="mat-slide" formControlName="enabled">
{{ '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-label translate>api-key.description</mat-label>
<textarea #input cdkTextareaAutosize matInput [formControl]="descriptionFormControl" rows="2" maxLength="255"></textarea>
<mat-error *ngIf="descriptionFormControl.hasError('required')">
{{ 'asset.description-required' | translate }}
</mat-error>
@if (descriptionFormControl.hasError('required')) {
<mat-error>
{{ 'asset.description-required' | translate }}
</mat-error>
}
<mat-hint align="end">{{ input.value?.length || 0 }}/255</mat-hint>
</mat-form-field>
<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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col gap-2">
<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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content class="flex flex-col">
<fieldset [disabled]="isLoading$ | async">
<mat-radio-group formControlName="addToDashboardType">
@ -46,9 +48,11 @@
<mat-label translate>dashboard.new-dashboard-title</mat-label>
<input matInput formControlName="newDashboardTitle"
[required]="addWidgetFormGroup.get('addToDashboardType').value === 1">
<mat-error *ngIf="addWidgetFormGroup.get('newDashboardTitle').hasError('required')">
{{ 'dashboard.title-required' | translate }}
</mat-error>
@if (addWidgetFormGroup.get('newDashboardTitle').hasError('required')) {
<mat-error>
{{ 'dashboard.title-required' | translate }}
</mat-error>
}
</mat-form-field>
</section>
</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="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>
<mat-form-field class="mat-block tb-attribute-scope" *ngIf="!disableAttributeScopeSelection && !attributeScopeSelectionReadonly">
<mat-label translate>attribute.attributes-scope</mat-label>
<mat-select [disabled]="(isLoading$ | async)"
[ngModel]="attributeScope"
(ngModelChange)="attributeScopeChanged($event)">
<mat-option *ngFor="let scope of attributeScopes" [value]="scope">
{{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
@if (!disableAttributeScopeSelection && !attributeScopeSelectionReadonly) {
<mat-form-field class="mat-block tb-attribute-scope">
<mat-label translate>attribute.attributes-scope</mat-label>
<mat-select [disabled]="(isLoading$ | async)"
[ngModel]="attributeScope"
(ngModelChange)="attributeScopeChanged($event)">
@for (scope of attributeScopes; track scope) {
<mat-option [value]="scope">
{{ telemetryTypeTranslationsMap.get(toTelemetryTypeFunc(scope)) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
</div>
<span class="flex-1"></span>
<button mat-icon-button
*ngIf="attributeScope !== attributeScopeTypes.CLIENT_SCOPE"
[disabled]="isLoading$ | async"
(click)="addAttribute($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
<button mat-icon-button
*ngIf="!isClientSideTelemetryTypeMap.get(attributeScope)"
[disabled]="isLoading$ | async"
(click)="reloadAttributes()"
matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above">
<mat-icon>refresh</mat-icon>
</button>
@if (attributeScope !== attributeScopeTypes.CLIENT_SCOPE) {
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="addAttribute($event)"
matTooltip="{{ 'action.add' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
}
@if (!isClientSideTelemetryTypeMap.get(attributeScope)) {
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="reloadAttributes()"
matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above">
<mat-icon>refresh</mat-icon>
</button>
}
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="enterFilterMode()"
@ -82,7 +88,7 @@
<div class="mat-toolbar-tools xs:flex xs:flex-row xs:flex-wrap">
<span class="tb-entity-table-info">
{{ (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 class="flex-1"></span>
<button [class.!hidden]="attributeScope === attributeScopeTypes.CLIENT_SCOPE"
@ -139,7 +145,7 @@
</mat-toolbar>
<div class="table-container flex-1" [class.!hidden]="mode === 'widget'">
<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>
<mat-header-cell *matHeaderCellDef style="width: 40px;">
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
@ -173,7 +179,7 @@
tooltipPosition="above"
icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
>
>
</tb-copy-button>
</div>
</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="overflow: hidden; align-items: center;">
<span class="ellipsis">{{attribute.value | tbJson}}</span>
<tb-copy-button
*ngIf="attribute.value !== ''"
class="attribute-copy"
[disabled]="isLoading$ | async"
[copyText]="attribute.value | tbJson"
tooltipText="{{ 'attribute.copy-value' | translate }}"
tooltipPosition="above"
icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
>
</tb-copy-button>
@if (attribute.value !== '') {
<tb-copy-button
class="attribute-copy"
[disabled]="isLoading$ | async"
[copyText]="attribute.value | tbJson"
tooltipText="{{ 'attribute.copy-value' | translate }}"
tooltipPosition="above"
icon="content_copy"
[style]="{padding: '4px', 'font-size': '16px', color: 'rgba(0,0,0,.87)'}"
>
</tb-copy-button>
}
</div>
<span [class.!hidden]="isClientSideTelemetryTypeMap.get(attributeScope)">
<mat-icon>edit</mat-icon>
</span>
<button mat-icon-button *ngIf="attributeScope === latestTelemetryTypes.LATEST_TELEMETRY"
(click)="deleteTimeseries($event, attribute)">
<mat-icon>delete</mat-icon>
</button>
@if (attributeScope === latestTelemetryTypes.LATEST_TELEMETRY) {
<button mat-icon-button
(click)="deleteTimeseries($event, attribute)">
<mat-icon>delete</mat-icon>
</button>
}
</div>
</mat-cell>
</ng-container>
<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)"
*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>
<span [class.!hidden]="(dataSource.isEmpty() | async) === false"
class="no-data-found flex items-center justify-center" translate>{{
attributeScope === latestTelemetryTypes.LATEST_TELEMETRY
? 'attribute.no-telemetry-text'
: 'attribute.no-attributes-text'
}}</span>
attributeScope === latestTelemetryTypes.LATEST_TELEMETRY
? 'attribute.no-telemetry-text'
: 'attribute.no-attributes-text'
}}</span>
</div>
<mat-divider [class.!hidden]="mode === 'widget'"></mat-divider>
<mat-paginator [class.!hidden]="mode === 'widget'"
@ -226,58 +235,65 @@
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="[10, 20, 30]"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator>
<ngx-hm-carousel *ngIf="mode === 'widget' && widgetsList.length > 0"
#carousel
[(ngModel)]="widgetsCarouselIndex"
(ngModelChange)="onWidgetsCarouselIndexChanged()"
[data]="widgetsList"
[infinite]="false"
class="carousel c-accent flex-1">
<section ngx-hm-carousel-container class="content">
<article class="item cursor-pointer"
ngx-hm-carousel-item
*ngFor="let widgets of widgetsList">
<tb-dashboard class="tb-absolute-fill"
[aliasController]="aliasController"
[widgets]="widgets"
[columns]="20"
[isEdit]="false"
[isMobileDisabled]="true"
[isEditActionEnabled]="false"
[isRemoveActionEnabled]="false">
</tb-dashboard>
</article>
</section>
<ng-template #carouselPrev>
<button mat-icon-button
*ngIf="widgetsCarouselIndex > 0"
matTooltip="{{ 'attribute.prev-widget' | translate }}"
matTooltipPosition="above">
<mat-icon>keyboard_arrow_left</mat-icon>
</button>
</ng-template>
<ng-template #carouselNext>
<button mat-icon-button
*ngIf="widgetsCarouselIndex < widgetsList.length - 1"
matTooltip="{{ 'attribute.next-widget' | translate }}"
matTooltipPosition="above">
<mat-icon>keyboard_arrow_right</mat-icon>
</button>
</ng-template>
<ng-template #carouselDot let-model>
<div class="ball"
[class.visible]="model.index === model.currentIndex"></div>
</ng-template>
</ngx-hm-carousel>
<span translate *ngIf="mode === 'widget' && widgetsLoaded &&
widgetsList.length === 0 &&
widgetBundleSet"
class="mat-headline-5 flex flex-1 items-center justify-center">widgets-bundle.empty</span>
<span translate *ngIf="mode === 'widget' && !widgetBundleSet"
class="mat-headline-5 flex flex-1 items-center justify-center">widget.select-widgets-bundle</span>
showFirstLastButtons></mat-paginator>
@if (mode === 'widget' && widgetsList.length > 0) {
<ngx-hm-carousel
#carousel
[(ngModel)]="widgetsCarouselIndex"
(ngModelChange)="onWidgetsCarouselIndexChanged()"
[data]="widgetsList"
[infinite]="false"
class="carousel c-accent flex-1">
<section ngx-hm-carousel-container class="content">
@for (widgets of widgetsList; track widgets) {
<article class="item cursor-pointer"
ngx-hm-carousel-item
>
<tb-dashboard class="tb-absolute-fill"
[aliasController]="aliasController"
[widgets]="widgets"
[columns]="20"
[isEdit]="false"
[isMobileDisabled]="true"
[isEditActionEnabled]="false"
[isRemoveActionEnabled]="false">
</tb-dashboard>
</article>
}
</section>
<ng-template #carouselPrev>
@if (widgetsCarouselIndex > 0) {
<button mat-icon-button
matTooltip="{{ 'attribute.prev-widget' | translate }}"
matTooltipPosition="above">
<mat-icon>keyboard_arrow_left</mat-icon>
</button>
}
</ng-template>
<ng-template #carouselNext>
@if (widgetsCarouselIndex < widgetsList.length - 1) {
<button mat-icon-button
matTooltip="{{ 'attribute.next-widget' | translate }}"
matTooltipPosition="above">
<mat-icon>keyboard_arrow_right</mat-icon>
</button>
}
</ng-template>
<ng-template #carouselDot let-model>
<div class="ball"
[class.visible]="model.index === model.currentIndex"></div>
</ng-template>
</ngx-hm-carousel>
}
@if (mode === 'widget' && widgetsLoaded &&
widgetsList.length === 0 &&
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>

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-label translate>attribute.delete-timeseries.strategy</mat-label>
<mat-select formControlName="strategy">
<mat-option *ngFor="let strategy of strategiesTranslationsMap.keys()" [value]="strategy">
{{ strategiesTranslationsMap.get(strategy) | translate }}
</mat-option>
@for (strategy of strategiesTranslationsMap.keys(); track strategy) {
<mat-option [value]="strategy">
{{ strategiesTranslationsMap.get(strategy) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
<div *ngIf="isPeriodStrategy()" class="tb-select-interval">
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>attribute.delete-timeseries.start-time</mat-label>
<mat-datetimepicker-toggle [for]="startDateTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startDateTimePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startDateTime" [matDatetimepicker]="startDateTimePicker">
</mat-form-field>
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>attribute.delete-timeseries.ends-on</mat-label>
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endDateTime" [matDatetimepicker]="endDatePicker">
</mat-form-field>
</div>
<mat-slide-toggle class="tb-slide-toggle" formControlName="rewriteLatest" *ngIf="isPeriodStrategy() || isDeleteLatestStrategy()">
{{ "attribute.delete-timeseries.rewrite-latest-value" | translate }}
</mat-slide-toggle>
@if (isPeriodStrategy()) {
<div class="tb-select-interval">
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>attribute.delete-timeseries.start-time</mat-label>
<mat-datetimepicker-toggle [for]="startDateTimePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #startDateTimePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="startDateTime" [matDatetimepicker]="startDateTimePicker">
</mat-form-field>
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-label translate>attribute.delete-timeseries.ends-on</mat-label>
<mat-datetimepicker-toggle [for]="endDatePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #endDatePicker type="datetime" openOnFocus="true"></mat-datetimepicker>
<input required matInput formControlName="endDateTime" [matDatetimepicker]="endDatePicker">
</mat-form-field>
</div>
}
@if (isPeriodStrategy() || isDeleteLatestStrategy()) {
<mat-slide-toggle class="tb-slide-toggle" formControlName="rewriteLatest">
{{ "attribute.delete-timeseries.rewrite-latest-value" | translate }}
</mat-slide-toggle>
}
</form>
<div class="tb-panel-actions flex flex-row">
<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.
-->
<div class="tb-details-buttons xs:flex xs:flex-col" *ngIf="!standalone">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{ 'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{ 'action.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{ 'action.delete' | translate }}
</button>
</div>
@if (!standalone) {
<div class="tb-details-buttons xs:flex xs:flex-col">
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'open')"
[class.!hidden]="isEdit || isDetailsPage">
{{ 'common.open-details-page' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'export')"
[class.!hidden]="isEdit">
{{ 'action.export' | translate }}
</button>
<button mat-raised-button color="primary"
[disabled]="(isLoading$ | async)"
(click)="onEntityAction($event, 'delete')"
[class.!hidden]="hideDelete() || isEdit">
{{ 'action.delete' | translate }}
</button>
</div>
}
<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-gap">
@ -55,11 +57,11 @@
<mat-hint></mat-hint>
</mat-form-field>
<tb-entity-debug-settings-button
formControlName="debugSettings"
style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
formControlName="debugSettings"
style="margin-bottom: 22px"
[entityType]="EntityType.CALCULATED_FIELD"
[additionalActionConfig]="additionalDebugActionConfig"
/>
</div>
<tb-entity-select formControlName="entityId"
appearance="outline"
@ -74,8 +76,8 @@
<mat-select formControlName="type">
<mat-select-trigger>
{{ CalculatedFieldTypeTranslations.has(entityForm.get('type').value)
? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate)
: entityForm.get('type').value }}
? (CalculatedFieldTypeTranslations.get(entityForm.get('type').value).name | translate)
: entityForm.get('type').value }}
</mat-select-trigger>
@for (type of fieldTypes; track type) {
<mat-option [value]="type" class="py-1">
@ -83,67 +85,67 @@
{{ CalculatedFieldTypeTranslations.get(type).name | translate }}
</span>
<br>
<small matListItemMeta style="color: inherit">
{{ CalculatedFieldTypeTranslations.get(type).hint | translate }}
</small>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@switch (entityForm.get('type').value) {
@case (CalculatedFieldType.GEOFENCING) {
<tb-geofencing-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="ownerId"
[tenantId]="tenantId"
[isEditValue]="isEditValue"
></tb-geofencing-configuration>
}
@case (CalculatedFieldType.PROPAGATION) {
<tb-propagation-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[ownerId]="ownerId"
[testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue"
></tb-propagation-configuration>
}
@case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) {
<tb-related-entities-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue"
></tb-related-entities-aggregation-component>
}
@case (CalculatedFieldType.ENTITY_AGGREGATION) {
<tb-entity-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[isEditValue]="isEditValue"
></tb-entity-aggregation-component>
}
@default {
<tb-simple-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="ownerId"
[tenantId]="tenantId"
[isScript]="entityForm.get('type').value === CalculatedFieldType.SCRIPT"
[testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue"
></tb-simple-configuration>
<small matListItemMeta style="color: inherit">
{{ CalculatedFieldTypeTranslations.get(type).hint | translate }}
</small>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@switch (entityForm.get('type').value) {
@case (CalculatedFieldType.GEOFENCING) {
<tb-geofencing-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="ownerId"
[tenantId]="tenantId"
[isEditValue]="isEditValue"
></tb-geofencing-configuration>
}
@case (CalculatedFieldType.PROPAGATION) {
<tb-propagation-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[ownerId]="ownerId"
[testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue"
></tb-propagation-configuration>
}
@case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) {
<tb-related-entities-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue"
></tb-related-entities-aggregation-component>
}
@case (CalculatedFieldType.ENTITY_AGGREGATION) {
<tb-entity-aggregation-component formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[tenantId]="tenantId"
[isEditValue]="isEditValue"
></tb-entity-aggregation-component>
}
@default {
<tb-simple-configuration formControlName="configuration"
[class.tb-form-panel-disabled]="disabledConfiguration"
[entityId]="entityId"
[entityName]="entityName"
[ownerId]="ownerId"
[tenantId]="tenantId"
[isScript]="entityForm.get('type').value === CalculatedFieldType.SCRIPT"
[testScript]="onTestScript.bind(this)"
[isEditValue]="isEditValue"
></tb-simple-configuration>
}
}
}
</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-label translate>calculated-fields.aggregate-interval-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of AggIntervalTypes" [value]="type">
{{ AggIntervalTypeTranslations.get(type) | translate }}
</mat-option>
@for (type of AggIntervalTypes; track type) {
<mat-option [value]="type">
{{ AggIntervalTypeTranslations.get(type) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
<tb-timezone-select
@ -128,7 +130,7 @@
<div class="tb-form-panel stroked">
<mat-slide-toggle class="mat-slide flex" formControlName="produceIntermediateResult">
<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 }}
</div>
@ -140,5 +142,5 @@
simpleMode
hiddenName
disableType
[entityId]="entityId"></tb-calculate-field-output>
[entityId]="entityId"></tb-calculate-field-output>
</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 [formGroup]="refDynamicSourceFormGroup">
<div class="tb-form-panel stroked" *ngIf="entityType === ArgumentEntityType.RelationQuery">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header>
<span class="tb-form-panel-title tb-required">{{ 'calculated-fields.entity-zone-relationship' | translate }}</span>
</mat-expansion-panel-header>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-actions-header"></div>
<div class="tb-form-table-header-cell" translate>calculated-fields.level</div>
<div class="tb-form-table-header-cell flex-1" translate>calculated-fields.direction-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 tb-actions-header"></div>
</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>
@if (entityType === ArgumentEntityType.RelationQuery) {
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings" expanded>
<mat-expansion-panel-header>
<span class="tb-form-panel-title tb-required">{{ 'calculated-fields.entity-zone-relationship' | translate }}</span>
</mat-expansion-panel-header>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div class="tb-form-table-header-cell tb-actions-header"></div>
<div class="tb-form-table-header-cell" translate>calculated-fields.level</div>
<div class="tb-form-table-header-cell flex-1" translate>calculated-fields.direction-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 tb-actions-header"></div>
</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>
@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>
} @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>
@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.
-->
<ng-container *ngIf="panelMode; else componentMode">
@if (panelMode) {
<ng-container *ngTemplateOutlet="calculatedFieldsFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else calculatedFieldsFilter">
} @else {
@if (buttonMode) {
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
@ -27,8 +26,49 @@
(click)="toggleCfFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
} @else {
<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>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="calculatedFieldsFilter"></ng-container>
@ -72,9 +112,11 @@
<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>
<mat-option *ngFor="let type of listEntityTypes" [value]="type">
{{ entityTypeTranslations.get(type)?.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>

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

@ -21,11 +21,13 @@
<h2>{{'widget.add' | translate}}:</h2>
<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;">
<tb-toggle-select *ngIf="widgetConfig.hasBasicMode" appearance="fill-invert" selectMediaBreakpoint="xs"
[(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<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>
@if (widgetConfig.hasBasicMode) {
<tb-toggle-select appearance="fill-invert" selectMediaBreakpoint="xs"
[(ngModel)]="widgetConfigMode" [ngModelOptions]="{standalone: true}">
<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>
</div>
@ -49,22 +51,24 @@
isAdd
formControlName="widgetConfig">
</tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
@if (previewMode) {
<tb-widget-preview class="tb-absolute-fill"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
<div class="tb-preview-panel-content">
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
</div>
</tb-widget-preview>
}
</div>
<div mat-dialog-actions class="flex items-center justify-between">
<button mat-button color="primary"
@ -74,14 +78,16 @@
{{ 'action.cancel' | translate }}
</button>
<div class="flex flex-row gap-2">
<button *ngIf="!previewMode"
mat-button color="primary"
type="button"
[disabled]="!widgetFormGroup.valid"
(click)="previewMode = true"
cdkFocusInitial>
{{ 'widget-config.preview' | translate }}
</button>
@if (!previewMode) {
<button
mat-button color="primary"
type="button"
[disabled]="!widgetFormGroup.valid"
(click)="previewMode = true"
cdkFocusInitial>
{{ 'widget-config.preview' | translate }}
</button>
}
<button mat-raised-button color="primary"
(click)="add()"
[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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content style="position: relative;">
<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">
@ -40,8 +42,11 @@
<input matInput formControlName="left" type="number" step="1" min="0" max="100">
</mat-form-field>
<div class="tb-image-preview-container">
<div *ngIf="!imageUrl; else elseBlock">{{ 'dashboard.no-image' | translate }}</div>
<ng-template #elseBlock><img class="tb-image-preview" [src]="imageUrl | image | async" /></ng-template>
@if (!imageUrl) {
<div>{{ 'dashboard.no-image' | translate }}</div>
} @else {
<img class="tb-image-preview" [src]="imageUrl | image | async" />
}
</div>
<mat-form-field class="rect-field">
<mat-label>Right %</mat-label>
@ -63,13 +68,15 @@
</div>
<div [formGroup]="dashboardImageFormGroup">
<tb-gallery-image-input label="{{'dashboard.image' | translate}}"
formControlName="dashboardImage">
formControlName="dashboardImage">
</tb-gallery-image-input>
</div>
</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>
</div>
</div>
}
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<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()">
<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">
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" class="gt-sm:!hidden" displayUserInfo="true">
</tb-user-menu>
<ng-container *ngIf="isEdit">
@if (!isPublicUser() && forceFullscreen) {
<tb-user-menu class="gt-sm:!hidden" displayUserInfo="true">
</tb-user-menu>
}
@if (isEdit) {
<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">
<mat-divider class="tb-toolbar-divider" vertical></mat-divider>
@ -48,7 +50,7 @@
[dashboardCtrl]="this">
</tb-select-dashboard-breakpoint>
</div>
</ng-container>
}
<tb-states-component class="lt-md:flex-1"
[statesControllerId]="isEdit ? 'default' : dashboardConfiguration.settings.stateControllerId"
[dashboardCtrl]="this"
@ -59,7 +61,7 @@
[syncStateWithQueryParam]="syncStateWithQueryParam"
[states]="dashboardConfiguration.states">
</tb-states-component>
<ng-container *ngIf="isEdit">
@if (isEdit) {
<button class="gt-md:!hidden" mat-icon-button
matTooltip="{{'dashboard.manage-states' | translate}}"
matTooltipPosition="below"
@ -73,8 +75,10 @@
<mat-icon>layers</mat-icon>
{{'dashboard.states-short' | translate}}
</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 class="tb-dashboard-action-panel">
<button [class.!hidden]="!showCloseToolbar()" mat-icon-button class="close-action"
@ -83,8 +87,10 @@
(click)="closeToolbar()">
<mat-icon>arrow_forward</mat-icon>
</button>
<tb-user-menu *ngIf="!isPublicUser() && forceFullscreen" class="lt-md:!hidden" displayUserInfo="true">
</tb-user-menu>
@if (!isPublicUser() && forceFullscreen) {
<tb-user-menu class="lt-md:!hidden" displayUserInfo="true">
</tb-user-menu>
}
<button [class.!hidden]="!showRightLayoutSwitch()" mat-icon-button
matTooltip="{{ (isRightLayoutOpened ? 'dashboard.hide-details' : 'dashboard.show-details') | translate }}"
matTooltipPosition="below"
@ -103,371 +109,399 @@
(click)="exportDashboard($event)">
<mat-icon>file_download</mat-icon>
</button>
<ng-container *ngIf="!readonly && !isEdit">
@if (!readonly && !isEdit) {
<button class="gt-md:!hidden" mat-icon-button (click)="toggleDashboardEditMode()"
matTooltip="{{'dashboard.edit-mode' | translate}}"
matTooltipPosition="below">
<mat-icon>edit</mat-icon></button>
<button class="lt-lg:!hidden" mat-stroked-button (click)="toggleDashboardEditMode()">
<mat-icon>edit</mat-icon>{{'dashboard.edit-mode' | translate}}</button>
</ng-container>
<ng-container *ngIf="isEdit">
<button class="gt-md:!hidden" mat-icon-button (click)="saveDashboard()" [disabled]="isLoading$ | async"
matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below">
<mat-icon>done</mat-icon></button>
<button class="lt-lg:!hidden" mat-stroked-button [disabled]="isLoading$ | async" (click)="saveDashboard()">
<mat-icon>done</mat-icon>{{ 'action.save' | translate }}</button>
<button class="gt-md:!hidden" mat-icon-button (click)="toggleDashboardEditMode()"
matTooltip="{{'action.cancel' | translate}}"
[disabled]="isLoading$ | async"
matTooltipPosition="below">
<mat-icon>close</mat-icon></button>
<button class="lt-lg:!hidden" mat-button (click)="toggleDashboardEditMode()" [disabled]="isLoading$ | async">
<mat-icon>close</mat-icon>{{ 'action.cancel' | translate }}</button>
<mat-divider class="tb-toolbar-divider" vertical></mat-divider>
</ng-container>
<ng-container *ngIf="currentDashboardId && isEdit && isTenantAdmin()">
<button class="lt-lg:!hidden gt-lg:!hidden" mat-icon-button
#versionControlIconButton
matTooltip="{{'version-control.version-control' | translate}}"
matTooltipPosition="below"
(click)="toggleVersionControl($event, versionControlIconButton)">
<mat-icon>history</mat-icon>
</button>
<button class="lt-xl:!hidden" mat-button
#versionControlButton
matTooltip="{{'version-control.version-control' | translate}}"
matTooltipPosition="below"
(click)="toggleVersionControl($event, versionControlButton)">
<mat-icon>history</mat-icon>
{{'version-control.versions' | translate}}
</button>
</ng-container>
<ng-container *ngIf="isEdit">
<button class="gt-lg:!hidden" mat-icon-button
matTooltip="{{ 'filter.filters' | translate }}"
matTooltipPosition="below"
(click)="openFilters($event)">
<mat-icon>filter_list</mat-icon>
</button>
<button class="lt-xl:!hidden" mat-button
matTooltip="{{ 'filter.filters' | translate }}"
matTooltipPosition="below"
(click)="openFilters($event)">
<mat-icon>filter_list</mat-icon>
{{ 'filter.filters' | translate }}
</button>
</ng-container>
<ng-container *ngIf="isEdit">
<button class="gt-lg:!hidden" mat-icon-button
matTooltip="{{ 'entity.aliases' | translate }}"
matTooltipPosition="below"
(click)="openEntityAliases($event)">
<mat-icon>devices_other</mat-icon>
</button>
<button class="lt-xl:!hidden" mat-button
matTooltip="{{ 'entity.aliases' | translate }}"
matTooltipPosition="below"
(click)="openEntityAliases($event)">
<mat-icon>devices_other</mat-icon>
{{ 'entity.aliases-short' | translate }}
</button>
</ng-container>
<ng-container *ngIf="isEdit">
<button class="gt-lg:!hidden" mat-icon-button
matTooltip="{{ 'dashboard.settings' | translate }}"
matTooltipPosition="below"
(click)="openDashboardSettings($event)">
<mat-icon>settings</mat-icon>
</button>
<button class="lt-xl:!hidden" mat-button
matTooltip="{{ 'dashboard.settings' | translate }}"
matTooltipPosition="below"
(click)="openDashboardSettings($event)">
<mat-icon>settings</mat-icon>
{{ 'dashboard.settings' | translate }}
</button>
</ng-container>
<button [class.!hidden]="!currentDashboardId || isEdit || isMobileApp || !isTenantAdmin() || !displayUpdateDashboardImage()" mat-icon-button
matTooltip="{{'dashboard.update-image' | translate}}"
matTooltipPosition="below"
(click)="updateDashboardImage($event)">
<mat-icon>wallpaper</mat-icon>
</button>
<ng-container *ngIf="isEdit; else timewindowTemplate">
<tb-timewindow class="lt-xl:!hidden" asButton
flatButton
showSaveAsDefault
[displayTimewindowValue]="false"
[isEdit]="true"
tooltipPosition="below"
aggregation="true"
timezone="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow">
</tb-timewindow>
<tb-timewindow class="gt-lg:!hidden"
hideLabel
showSaveAsDefault
[isEdit]="true"
tooltipPosition="below"
aggregation="true"
timezone="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow">
</tb-timewindow>
</ng-container>
<ng-template #timewindowTemplate>
<ng-container *ngIf="displayDashboardTimewindow()">
<tb-timewindow class="gt-md:!hidden"
[isEdit]="false"
hideLabel
tooltipPosition="below"
aggregation="true"
timezone="true"
[showSaveAsDefault]="!readonly"
[(ngModel)]="dashboardCtx.dashboardTimewindow"
(saveAsDefault)="saveDashboard()">>
</tb-timewindow>
<tb-timewindow class="lt-lg:!hidden"
[isEdit]="false"
tooltipPosition="below"
aggregation="true"
timezone="true"
[showSaveAsDefault]="!readonly"
[(ngModel)]="dashboardCtx.dashboardTimewindow"
(saveAsDefault)="saveDashboard()">>
</tb-timewindow>
</ng-container>
</ng-template>
<ng-container *ngIf="isEdit">
<button class="gt-md:!hidden" mat-icon-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below"
(click)="addWidget($event)">
<mat-icon>add</mat-icon>
</button>
<button class="lt-lg:!hidden" mat-stroked-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below"
(click)="addWidget($event)">
<mat-icon>add</mat-icon>
{{ 'dashboard.add-widget-button-text' | translate }}
</button>
</ng-container>
<tb-filters-edit [class.!hidden]="isEdit || !displayFilters()"
tooltipPosition="below"
[aliasController]="dashboardCtx.aliasController">
</tb-filters-edit>
<tb-aliases-entity-select [class.!hidden]="isEdit || !displayEntitiesSelect()"
tooltipPosition="below"
[aliasController]="dashboardCtx.aliasController">
</tb-aliases-entity-select>
<tb-dashboard-select *ngIf="!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()"
[(ngModel)]="currentDashboardId"
(ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
[customerId]="currentCustomerId"
[dashboardsScope]="currentDashboardScope">
</tb-dashboard-select>
</div>
</div>
</tb-dashboard-toolbar>
</section>
<section class="tb-dashboard-container tb-absolute-fill"
tb-toast toastTarget="dashboardRoot"
#dashboardContainer
[class.is-fullscreen]="forceFullscreen"
[class.tb-dashboard-toolbar-opened]="toolbarOpened"
[class.tb-dashboard-toolbar-animated]="isToolbarOpenedAnimate"
[class.tb-dashboard-toolbar-closed]="!toolbarOpened">
<mat-drawer-container hasBackdrop="false" class="tb-absolute-fill tb-dashboard-drawer-container">
<mat-drawer-content #dashboardContent class="flex flex-col items-start justify-center">
<section *ngIf="!widgetEditMode" class="tb-dashboard-title"
[style.color]="dashboard.configuration.settings.titleColor">
<h3 [class.!hidden]="isEdit || !displayTitle()">{{ translatedDashboardTitle }}</h3>
<mat-form-field [class.!hidden]="!isEdit" class="mat-block tb-appearance-transparent" subscriptSizing="dynamic">
<mat-label translate [style.color]="dashboard.configuration.settings.titleColor">dashboard.title</mat-label>
<input matInput class="tb-dashboard-title"
[style.color]="dashboard.configuration.settings.titleColor"
required name="title" [(ngModel)]="dashboard.title">
</mat-form-field>
</section>
<mat-drawer-container class="tb-dashboard-layouts flex-1"
[class.tb-shrinked]="isEditingWidget">
<mat-drawer *ngIf="layouts.right.show"
id="tb-right-layout"
style="border-left: none"
[style.min-width]="rightLayoutSize.width"
[style.max-width]="rightLayoutSize.width"
[style.height]="rightLayoutSize.height"
disableClose="true"
[@.disabled]="!isMobile"
position="end"
[mode]="isMobile ? 'over' : 'side'"
[(opened)]="rightLayoutOpened">
<tb-dashboard-layout style="height: 100%;"
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.main.layoutCtx : layouts.right.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode"
[parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout>
</mat-drawer>
<mat-drawer-content [class.!hidden]="!layouts.main.show"
id="tb-main-layout"
[style.width]="mainLayoutSize.width"
[style.height]="mainLayoutSize.height">
<tb-dashboard-layout
[style.max-width]="mainLayoutSize.maxWidth"
[style.min-width]="mainLayoutSize.minWidth"
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode"
[parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout>
<div class="layout-filler"
*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"
<button class="lt-lg:!hidden" mat-stroked-button (click)="toggleDashboardEditMode()">
<mat-icon>edit</mat-icon>{{'dashboard.edit-mode' | translate}}</button>
}
@if (isEdit) {
<button class="gt-md:!hidden" mat-icon-button (click)="saveDashboard()" [disabled]="isLoading$ | async"
matTooltip="{{'action.save' | translate}}"
matTooltipPosition="below">
<mat-icon>done</mat-icon></button>
<button class="lt-lg:!hidden" mat-stroked-button [disabled]="isLoading$ | async" (click)="saveDashboard()">
<mat-icon>done</mat-icon>{{ 'action.save' | translate }}</button>
<button class="gt-md:!hidden" mat-icon-button (click)="toggleDashboardEditMode()"
matTooltip="{{'action.cancel' | translate}}"
[disabled]="isLoading$ | async"
matTooltipPosition="below">
<mat-icon>close</mat-icon></button>
<button class="lt-lg:!hidden" mat-button (click)="toggleDashboardEditMode()" [disabled]="isLoading$ | async">
<mat-icon>close</mat-icon>{{ 'action.cancel' | translate }}</button>
<mat-divider class="tb-toolbar-divider" vertical></mat-divider>
}
@if (currentDashboardId && isEdit && isTenantAdmin()) {
<button class="lt-lg:!hidden gt-lg:!hidden" mat-icon-button
#versionControlIconButton
matTooltip="{{'version-control.version-control' | translate}}"
matTooltipPosition="below"
(click)="toggleVersionControl($event, versionControlIconButton)">
<mat-icon>history</mat-icon>
</button>
<button class="lt-xl:!hidden" mat-button
#versionControlButton
matTooltip="{{'version-control.version-control' | translate}}"
matTooltipPosition="below"
(click)="toggleVersionControl($event, versionControlButton)">
<mat-icon>history</mat-icon>
{{'version-control.versions' | translate}}
</button>
}
@if (isEdit) {
<button class="gt-lg:!hidden" mat-icon-button
matTooltip="{{ 'filter.filters' | translate }}"
matTooltipPosition="below"
(click)="openFilters($event)">
<mat-icon>filter_list</mat-icon>
</button>
<button class="lt-xl:!hidden" mat-button
matTooltip="{{ 'filter.filters' | translate }}"
matTooltipPosition="below"
(click)="openFilters($event)">
<mat-icon>filter_list</mat-icon>
{{ 'filter.filters' | translate }}
</button>
}
@if (isEdit) {
<button class="gt-lg:!hidden" mat-icon-button
matTooltip="{{ 'entity.aliases' | translate }}"
matTooltipPosition="below"
(click)="openEntityAliases($event)">
<mat-icon>devices_other</mat-icon>
</button>
<button class="lt-xl:!hidden" mat-button
matTooltip="{{ 'entity.aliases' | translate }}"
matTooltipPosition="below"
(click)="openEntityAliases($event)">
<mat-icon>devices_other</mat-icon>
{{ 'entity.aliases-short' | translate }}
</button>
}
@if (isEdit) {
<button class="gt-lg:!hidden" mat-icon-button
matTooltip="{{ 'dashboard.settings' | translate }}"
matTooltipPosition="below"
(click)="openDashboardSettings($event)">
<mat-icon>settings</mat-icon>
</button>
<button class="lt-xl:!hidden" mat-button
matTooltip="{{ 'dashboard.settings' | translate }}"
matTooltipPosition="below"
(click)="openDashboardSettings($event)">
<mat-icon>settings</mat-icon>
{{ 'dashboard.settings' | translate }}
</button>
}
<button [class.!hidden]="!currentDashboardId || isEdit || isMobileApp || !isTenantAdmin() || !displayUpdateDashboardImage()" mat-icon-button
matTooltip="{{'dashboard.update-image' | translate}}"
matTooltipPosition="below"
(click)="updateDashboardImage($event)">
<mat-icon>wallpaper</mat-icon>
</button>
@if (isEdit) {
<tb-timewindow class="lt-xl:!hidden" asButton
flatButton
showSaveAsDefault
[displayTimewindowValue]="false"
[isEdit]="true"
tooltipPosition="below"
aggregation="true"
timezone="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow">
</tb-timewindow>
<tb-timewindow class="gt-lg:!hidden"
hideLabel
showSaveAsDefault
[isEdit]="true"
tooltipPosition="below"
aggregation="true"
timezone="true"
[(ngModel)]="dashboardCtx.dashboardTimewindow">
</tb-timewindow>
} @else {
@if (displayDashboardTimewindow()) {
<tb-timewindow class="gt-md:!hidden"
[isEdit]="false"
hideLabel
tooltipPosition="below"
aggregation="true"
timezone="true"
[showSaveAsDefault]="!readonly"
[(ngModel)]="dashboardCtx.dashboardTimewindow"
(saveAsDefault)="saveDashboard()">>
</tb-timewindow>
<tb-timewindow class="lt-lg:!hidden"
[isEdit]="false"
tooltipPosition="below"
aggregation="true"
timezone="true"
[showSaveAsDefault]="!readonly"
[(ngModel)]="dashboardCtx.dashboardTimewindow"
(saveAsDefault)="saveDashboard()">>
</tb-timewindow>
}
}
@if (isEdit) {
<button class="gt-md:!hidden" mat-icon-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below"
(click)="addWidget($event)">
<mat-icon>add</mat-icon>
</button>
<button class="lt-lg:!hidden" mat-stroked-button
matTooltip="{{ 'dashboard.add-widget-button-text' | translate }}"
matTooltipPosition="below"
(click)="addWidget($event)">
<mat-icon>add</mat-icon>
{{ 'dashboard.add-widget-button-text' | translate }}
</button>
}
<tb-filters-edit [class.!hidden]="isEdit || !displayFilters()"
tooltipPosition="below"
[aliasController]="dashboardCtx.aliasController">
</tb-filters-edit>
<tb-aliases-entity-select [class.!hidden]="isEdit || !displayEntitiesSelect()"
tooltipPosition="below"
[aliasController]="dashboardCtx.aliasController">
</tb-aliases-entity-select>
@if (!isEdit && !widgetEditMode && !embedded && displayDashboardsSelect()) {
<tb-dashboard-select
[(ngModel)]="currentDashboardId"
(ngModelChange)="currentDashboardIdChanged(currentDashboardId)"
[customerId]="currentCustomerId"
[dashboardsScope]="currentDashboardScope">
</tb-dashboard-select>
}
</div>
</div>
</tb-dashboard-toolbar>
</section>
<section class="tb-dashboard-container tb-absolute-fill"
tb-toast toastTarget="dashboardRoot"
#dashboardContainer
[class.is-fullscreen]="forceFullscreen"
[class.tb-dashboard-toolbar-opened]="toolbarOpened"
[class.tb-dashboard-toolbar-animated]="isToolbarOpenedAnimate"
[class.tb-dashboard-toolbar-closed]="!toolbarOpened">
<mat-drawer-container hasBackdrop="false" class="tb-absolute-fill tb-dashboard-drawer-container">
<mat-drawer-content #dashboardContent class="flex flex-col items-start justify-center">
@if (!widgetEditMode) {
<section class="tb-dashboard-title"
[style.color]="dashboard.configuration.settings.titleColor">
<h3 [class.!hidden]="isEdit || !displayTitle()">{{ translatedDashboardTitle }}</h3>
<mat-form-field [class.!hidden]="!isEdit" class="mat-block tb-appearance-transparent" subscriptSizing="dynamic">
<mat-label translate [style.color]="dashboard.configuration.settings.titleColor">dashboard.title</mat-label>
<input matInput class="tb-dashboard-title"
[style.color]="dashboard.configuration.settings.titleColor"
required name="title" [(ngModel)]="dashboard.title">
</mat-form-field>
</section>
}
<mat-drawer-container class="tb-dashboard-layouts flex-1"
[class.tb-shrinked]="isEditingWidget">
@if (layouts.right.show) {
<mat-drawer
id="tb-right-layout"
style="border-left: none"
[style.min-width]="rightLayoutSize.width"
[style.max-width]="rightLayoutSize.width"
[style.height]="rightLayoutSize.height"
disableClose="true"
[@.disabled]="!isMobile"
position="end"
[mode]="isMobile ? 'over' : 'side'"
[(opened)]="rightLayoutOpened">
<tb-dashboard-layout style="height: 100%;"
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.main.layoutCtx : layouts.right.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode"
[parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout>
</mat-drawer>
}
<mat-drawer-content [class.!hidden]="!layouts.main.show"
id="tb-main-layout"
[style.width]="mainLayoutSize.width"
[style.height]="mainLayoutSize.height">
<tb-dashboard-layout
[style.max-width]="mainLayoutSize.maxWidth"
[style.min-width]="mainLayoutSize.minWidth"
[dashboardCheatSheet]="cheatSheetComponent"
[layoutCtx]="mobileDisplayRightLayoutFirst ? layouts.right.layoutCtx : layouts.main.layoutCtx"
[dashboardCtx]="dashboardCtx"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isMobile]="forceDashboardMobileMode"
[widgetEditMode]="widgetEditMode"
[widget]="editingWidget"
[widgetLayout]="editingWidgetLayout"
[showLayoutConfig]="showLayoutConfigInEdit(editingLayoutCtx)"
[isDefaultBreakpoint]="editingLayoutCtx?.breakpoint === 'default'"
(revertWidgetConfig)="onRevertWidgetEdit()"
(applyWidgetConfig)="saveWidget()">
</tb-edit-widget>
</tb-details-panel>
<tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" class="flex-1"
[parentDashboard]="parentDashboard"
[popoverComponent]="popoverComponent">
</tb-dashboard-layout>
@if (isEdit) {
<div class="layout-filler"
[class.!hidden]="!showMainLayoutFiller">
{{ 'dashboard.breakpoint-value' | translate : {value: currentBreakpointValue } }}
</div>
}
</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 ?
((dashboardWidgetSelectComponent?.selectWidgetMode === 'allWidgets' ? ('dashboard.select-widget-value' | translate: { title: ('widget.all-widgets' | translate) }) :
(!dashboardWidgetSelectComponent?.widgetsBundle ?
'widget.select-widgets-bundle' : 'dashboard.select-widget-value') | translate: dashboardWidgetSelectComponent?.widgetsBundle)) : ''
}}"
headerHeightPx="64"
[isShowSearch]="true"
[isReadOnly]="true"
[isEdit]="false"
backgroundColor="#cfd8dc"
(closeDetails)="onAddWidgetClosed()"
(closeSearch)="dashboardWidgetSelectComponent.search = ''">
<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()">
<mat-icon>arrow_back</mat-icon>
</button>
</div>
<div class="search-pane flex flex-row" *ngIf="isAddingWidget && dashboardWidgetSelectComponent">
<tb-widgets-bundle-search class="flex-1"
[(ngModel)]="dashboardWidgetSelectComponent.search"
headerHeightPx="64"
[isShowSearch]="true"
[isReadOnly]="true"
[isEdit]="false"
backgroundColor="#cfd8dc"
(closeDetails)="onAddWidgetClosed()"
(closeSearch)="dashboardWidgetSelectComponent.search = ''">
<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()">
<mat-icon>arrow_back</mat-icon>
</button>
</div>
@if (isAddingWidget && dashboardWidgetSelectComponent) {
<div class="search-pane flex flex-row">
<tb-widgets-bundle-search class="flex-1"
[(ngModel)]="dashboardWidgetSelectComponent.search"
placeholder="{{ ((!dashboardWidgetSelectComponent.widgetsBundle && dashboardWidgetSelectComponent.selectWidgetMode === 'bundles')
? 'widgets-bundle.search' : 'widget.search') | translate }}">
</tb-widgets-bundle-search>
</div>
<div class="details-buttons flex flex-row items-center justify-start" *ngIf="isAddingWidget">
<button mat-button type="button" (click)="importWidget($event)"
*ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle">
<mat-icon>file_upload</mat-icon>{{ 'dashboard.import-widget' | translate }}</button>
<tb-toggle-select *ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle"
appearance="fill-invert"
disablePagination
selectMediaBreakpoint="xs"
[(ngModel)]="dashboardWidgetSelectComponent.selectWidgetMode">
<tb-toggle-option value="bundles">{{ 'widgets-bundle.widgets-bundles' | translate }}</tb-toggle-option>
<tb-toggle-option value="allWidgets">{{ 'widget.all-widgets' | translate }}</tb-toggle-option>
</tb-toggle-select>
<button mat-icon-button type="button"
*ngIf="dashboardWidgetSelectComponent?.widgetTypes.size > 1"
(click)="editWidgetsTypesToDisplay($event)"
matTooltip="{{ 'widget.filter' | translate }}"
matTooltipPosition="above">
<mat-icon>filter_list</mat-icon>
</button>
<tb-toggle-select *ngIf="dashboardWidgetSelectComponent?.hasDeprecated"
appearance="fill-invert"
disablePagination
selectMediaBreakpoint="xs"
[(ngModel)]="dashboardWidgetSelectComponent.deprecatedFilter">
<tb-toggle-option value="ALL">{{ 'widget.all' | translate }}</tb-toggle-option>
<tb-toggle-option value="ACTUAL">{{ 'widget.actual' | translate }}</tb-toggle-option>
<tb-toggle-option value="DEPRECATED">{{ 'widget.deprecated' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-dashboard-widget-select #dashboardWidgetSelect
*ngIf="isAddingWidget"
[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>
</tb-widgets-bundle-search>
</div>
}
@if (isAddingWidget) {
<div class="details-buttons flex flex-row items-center justify-start">
@if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) {
<button mat-button type="button" (click)="importWidget($event)"
>
<mat-icon>file_upload</mat-icon>{{ 'dashboard.import-widget' | translate }}</button>
}
@if (dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle) {
<tb-toggle-select
appearance="fill-invert"
disablePagination
selectMediaBreakpoint="xs"
[(ngModel)]="dashboardWidgetSelectComponent.selectWidgetMode">
<tb-toggle-option value="bundles">{{ 'widgets-bundle.widgets-bundles' | translate }}</tb-toggle-option>
<tb-toggle-option value="allWidgets">{{ 'widget.all-widgets' | translate }}</tb-toggle-option>
</tb-toggle-select>
}
@if (dashboardWidgetSelectComponent?.widgetTypes.size > 1) {
<button mat-icon-button type="button"
(click)="editWidgetsTypesToDisplay($event)"
matTooltip="{{ 'widget.filter' | translate }}"
matTooltipPosition="above">
<mat-icon>filter_list</mat-icon>
</button>
}
@if (dashboardWidgetSelectComponent?.hasDeprecated) {
<tb-toggle-select
appearance="fill-invert"
disablePagination
selectMediaBreakpoint="xs"
[(ngModel)]="dashboardWidgetSelectComponent.deprecatedFilter">
<tb-toggle-option value="ALL">{{ 'widget.all' | translate }}</tb-toggle-option>
<tb-toggle-option value="ACTUAL">{{ 'widget.actual' | translate }}</tb-toggle-option>
<tb-toggle-option value="DEPRECATED">{{ 'widget.deprecated' | translate }}</tb-toggle-option>
</tb-toggle-select>
}
</div>
}
@if (isAddingWidget) {
<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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<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">
<div class="tb-form-row space-between">
<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>
@if (settings) {
<div class="tb-form-panel no-border no-padding" [formGroup]="settingsFormGroup">
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitle">
{{ '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>
<div>{{ 'dashboard.state-controller' | translate }}</div>
<mat-form-field class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="viewFormat">
<mat-option *ngFor="let type of viewFormatTypes" [value]="type">
{{ viewFormatTypeTranslationMap.get(type) | translate }}
</mat-option>
<mat-select required formControlName="stateControllerId">
@for (stateControllerId of stateControllerIds; track stateControllerId) {
<mat-option [value]="stateControllerId">
{{ getStatesControllerTranslation(stateControllerId) }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div translate>dashboard.columns-count</div>
<mat-form-field *ngIf="!isScada()" 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)"
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.title-settings</div>
<div class="tb-form-row space-between">
<mat-slide-toggle class="mat-slide" formControlName="showTitle">
{{ '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>
}
@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' :
(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"
required
placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<mat-form-field *ngIf="isScada()"
class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="columns">
<mat-option *ngFor="let columns of scadaColumns" [value]="columns">
{{ columns }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<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)"
<input matInput formControlName="columns" type="number" min="10" max="1008" step="1"
required
placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
}
@if (isScada()) {
<mat-form-field
class="medium-width" appearance="outline" subscriptSizing="dynamic">
<mat-select required formControlName="columns">
@for (columns of scadaColumns; track columns) {
<mat-option [value]="columns">
{{ columns }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
</div>
<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' :
(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"
required
placeholder="{{ 'widget-config.set' | translate }}">
<span translate matSuffix>dashboard.columns-suffix</span>
</mat-form-field>
</div>
<div *ngIf="!isScada()" class="tb-form-row space-between">
<div translate>dashboard.widgets-margins</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched)"
<input matInput formControlName="minColumns" type="number" min="10" max="1008" step="1"
required
placeholder="{{ 'widget-config.set' | translate }}">
<span translate matSuffix>dashboard.columns-suffix</span>
</mat-form-field>
</div>
@if (!isScada()) {
<div class="tb-form-row space-between">
<div translate>dashboard.widgets-margins</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched)"
[matTooltip]="(gridSettingsFormGroup.get('margin').hasError('required') ? 'dashboard.margin-required' :
(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"
required
placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div *ngIf="!isScada()" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="outerMargin">
{{ 'dashboard.apply-outer-margin' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="!isScada()" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoFillHeight">
{{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle>
</div>
<div *ngIf="isDefaultLayout && !isDefaultBreakpoint" class="tb-form-row space-between">
<div translate>dashboard.row-height</div>
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched)"
<input matInput formControlName="margin" type="number" min="0" max="50" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
}
@if (!isScada()) {
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="outerMargin">
{{ 'dashboard.apply-outer-margin' | translate }}
</mat-slide-toggle>
</div>
}
@if (!isScada()) {
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="autoFillHeight">
{{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle>
</div>
}
@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' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate">
<input matInput formControlName="rowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
</div>
<div class="tb-form-row space-between">
<div translate>dashboard.background-color</div>
<tb-color-input asBoxInput
colorClearButton
formControlName="backgroundColor">
</tb-color-input>
<input matInput formControlName="rowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
</div>
}
<div class="tb-form-row space-between">
<div translate>dashboard.background-color</div>
<tb-color-input asBoxInput
colorClearButton
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>
<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 *ngIf="!isScada() && isDefaultBreakpoint" class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.mobile-layout</div>
<div *ngIf="isRightLayout" class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="mobileDisplayLayoutFirst">
{{ 'dashboard.display-first-in-mobile-view' | translate }}
</mat-slide-toggle>
</div>
<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)"
@if (!isScada() && isDefaultBreakpoint) {
<div class="tb-form-panel stroked">
<div class="tb-form-panel-title" translate>dashboard.mobile-layout</div>
@if (isRightLayout) {
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="mobileDisplayLayoutFirst">
{{ 'dashboard.display-first-in-mobile-view' | translate }}
</mat-slide-toggle>
</div>
}
@if (!isScada()) {
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="mobileAutoFillHeight">
{{ 'dashboard.autofill-height' | translate }}
</mat-slide-toggle>
</div>
}
@if (!isScada()) {
<div 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' :
(gridSettingsFormGroup.get('mobileRowHeight').hasError('min') ?
'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"
required
placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
</div>
<input matInput formControlName="mobileRowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<span matSuffix>px</span>
</mat-form-field>
</div>
}
</div>
}
</div>
</div>
}
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<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.
-->
<tb-dashboard-page *ngIf="stateExists"
[embedded]="true"
[syncStateWithQueryParam]="false"
[hideToolbar]="true"
[currentState]="currentState"
[dashboard]="dashboard"
[parentDashboard]="parentDashboard"
[parentAliasController]="parentAliasController">
</tb-dashboard-page>
<div class="tb-absolute-fill tb-widget-error" *ngIf="!stateExists">
<span>{{ 'dashboard.non-existent-dashboard-state-error' | translate:{stateId} }}</span>
</div>
@if (stateExists) {
<tb-dashboard-page
[embedded]="true"
[syncStateWithQueryParam]="false"
[hideToolbar]="true"
[currentState]="currentState"
[dashboard]="dashboard"
[parentDashboard]="parentDashboard"
[parentAliasController]="parentAliasController">
</tb-dashboard-page>
}
@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">
<ng-container *ngIf="selectWidgetMode === 'bundles'; else allWidgets">
<ng-container *ngIf="widgetsBundle; else bundles">
@if (selectWidgetMode === 'bundles') {
@if (widgetsBundle) {
<ng-container *ngTemplateOutlet="widgets; context:{ fetchFunction: widgetsFetchFunction, filter: widgetsFilter }"></ng-container>
</ng-container>
</ng-container>
<ng-template #allWidgets>
} @else {
<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">
@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-template>
}
<ng-template #widgets let-fetchFunction="fetchFunction" let-filter="filter">
<tb-scroll-grid
[columns]="columns"
@ -46,7 +87,9 @@
<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>
<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 class="preview-container">
@ -64,51 +107,10 @@
</div>
</ng-template>
<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 #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>
<mat-card class="tb-widget-preview-card loading-cell flex flex-col gap-2" appearance="raised">
<div class="title-container">

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

@ -28,56 +28,63 @@
[isDefaultBreakpoint]="isDefaultBreakpoint"
formControlName="widgetConfig">
<div class="tb-widget-config-header-prefix">
<button mat-button
*ngIf="previewMode"
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
<button *ngIf="!previewMode"
class="gt-sm:!hidden"
[disabled]="!widgetFormGroup.valid"
mat-button color="primary"
(click)="previewMode = true">
<mat-icon>visibility</mat-icon>
{{ 'widget-config.preview' | translate }}
</button>
@if (previewMode) {
<button mat-button
(click)="previewMode = false">
<mat-icon>chevron_left</mat-icon>
{{ 'action.back' | translate }}
</button>
}
@if (!previewMode) {
<button
class="gt-sm:!hidden"
[disabled]="!widgetFormGroup.valid"
mat-button color="primary"
(click)="previewMode = true">
<mat-icon>visibility</mat-icon>
{{ 'widget-config.preview' | translate }}
</button>
}
</div>
<div class="tb-widget-config-header-suffix flex flex-row items-center justify-end gap-3 lt-md:flex-1">
<button *ngIf="!previewMode"
class="lt-md:!hidden"
[disabled]="!widgetFormGroup.valid"
mat-button color="primary"
(click)="previewMode = true">
<mat-icon>visibility</mat-icon>
{{ 'widget-config.preview' | translate }}
</button>
@if (!previewMode) {
<button
class="lt-md:!hidden"
[disabled]="!widgetFormGroup.valid"
mat-button color="primary"
(click)="previewMode = true">
<mat-icon>visibility</mat-icon>
{{ 'widget-config.preview' | translate }}
</button>
}
<div class="flex flex-row items-center justify-start gap-3">
<button [disabled]="!widgetFormGroup.dirty"
mat-stroked-button color="primary"
(click)="onRevertWidgetConfig()">
mat-stroked-button color="primary"
(click)="onRevertWidgetConfig()">
<mat-icon>close</mat-icon>
{{ 'action.decline' | translate }}
</button>
<button [disabled]="!widgetFormGroup.valid || !widgetFormGroup.dirty"
mat-flat-button color="primary"
(click)="onApplyWidgetConfig()">
mat-flat-button color="primary"
(click)="onApplyWidgetConfig()">
<mat-icon>done</mat-icon>
{{ 'action.apply' | translate }}
</button>
</div>
</div>
</tb-widget-config>
<div *ngIf="previewMode" class="widget-preview-background">
<tb-widget-preview class="widget-preview-section"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
</tb-widget-preview>
</div>
@if (previewMode) {
<div class="widget-preview-background">
<tb-widget-preview class="widget-preview-section"
[aliasController]="aliasController"
[stateController]="stateController"
[dashboardTimewindow]="dashboard.configuration.timewindow"
[widget]="widget"
[widgetConfig]="widgetFormGroup.get('widgetConfig').value.config"
[previewWidth]="widgetConfig.typeParameters.previewWidth"
[previewHeight]="widgetConfig.typeParameters.previewHeight">
</tb-widget-preview>
</div>
}
</fieldset>
</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-select formControlName="newBreakpointId" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(addBreakpointFormGroup.get('newBreakpointId').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of allowBreakpointIds" [value]="breakpoint" style="min-height:56px;">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
@for (breakpoint of allowBreakpointIds; track breakpoint) {
<mat-option [value]="breakpoint" style="min-height:56px;">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" subscriptSizing="dynamic">
<mat-label translate>layout.copy-from</mat-label>
<mat-select formControlName="copyFrom" [hideSingleSelectionIndicator]="false">
<mat-select-trigger>{{ getName(addBreakpointFormGroup.get('copyFrom').value) }}</mat-select-trigger>
<mat-option *ngFor="let breakpoint of selectedBreakpointIds" [value]="breakpoint" style="min-height:56px;">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
@for (breakpoint of selectedBreakpointIds; track breakpoint) {
<mat-option [value]="breakpoint" style="min-height:56px;">
<mat-icon><tb-icon>{{ getIcon(breakpoint) }}</tb-icon></mat-icon>
<div>{{ getName(breakpoint) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpoint) }}</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</form>

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

@ -15,28 +15,36 @@
limitations under the License.
-->
<div class="tb-progress-cover flex flex-col items-center justify-center"
*ngIf="layoutCtx.widgets.isLoading()">
<mat-spinner color="warn" mode="indeterminate" diameter="100">
</mat-spinner>
</div>
@if (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>
</div>
}
<div class="mat-content"
style="position: relative; width: 100%; height: 100%;" [tb-hotkeys]="hotKeys"
[cheatSheet]="dashboardCheatSheet"
[style.background-image]="backgroundImage$ | async"
[style]="dashboardStyle">
<section *ngIf="layoutCtx.widgets.isEmpty()"
style="z-index: 1; pointer-events: none;"
class="mat-headline-5 tb-absolute-fill flex items-center justify-center">
<span *ngIf="!isEdit" style="text-align: center;">
{{'dashboard.no-widgets' | translate}}
</span>
<button mat-button *ngIf="isEdit && !widgetEditMode" class="tb-add-new-widget"
(click)="addWidget($event)">
<mat-icon class="tb-mat-96">add</mat-icon>
<span>{{ 'dashboard.add-widget' | translate }}</span>
</button>
</section>
style="position: relative; width: 100%; height: 100%;" [tb-hotkeys]="hotKeys"
[cheatSheet]="dashboardCheatSheet"
[style.background-image]="backgroundImage$ | async"
[style]="dashboardStyle">
@if (layoutCtx.widgets.isEmpty()) {
<section
style="z-index: 1; pointer-events: none;"
class="mat-headline-5 tb-absolute-fill flex items-center justify-center">
@if (!isEdit) {
<span style="text-align: center;">
{{'dashboard.no-widgets' | translate}}
</span>
}
@if (isEdit && !widgetEditMode) {
<button mat-button class="tb-add-new-widget"
(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"
[backgroundImage]="backgroundImage$ | async"
[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>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
@ -31,201 +31,226 @@
<div class="fixed-title-width" translate>dashboard.layout</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="layoutType">
<mat-option *ngFor="let type of layoutTypes" [value]="type">
{{ layoutTypeTranslations.get(type) | translate }}
</mat-option>
@for (type of layoutTypes; track type) {
<mat-option [value]="type">
{{ layoutTypeTranslations.get(type) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div *ngIf="!isDividerLayout; else dividerLayout" class="tb-form-panel stroked">
<div class="tb-form-table tb-layout-breakpoints">
<div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell tb-icon"></div>
<div class="tb-form-table-header-cell tb-breakpoint" translate>layout.breakpoints</div>
<div class="tb-form-table-header-cell tb-size" translate>layout.size</div>
<div class="tb-form-table-header-cell tb-actions"></div>
</div>
<div class="tb-form-table-body no-gap">
<ng-container *ngFor="let breakpoint of layoutBreakpoints; let last = last">
<div class="tb-form-table-row no-padding-right">
<div class="tb-form-table-row-cell tb-icon">
<tb-icon>{{ breakpoint.icon }}</tb-icon>
</div>
<div class="tb-form-table-row-cell tb-breakpoint">
{{ breakpoint.name }}
</div>
<div class="tb-form-table-row-cell tb-size" tbTruncateWithTooltip>
{{ breakpoint.descriptionSize }}
@if (!isDividerLayout) {
<div class="tb-form-panel stroked">
<div class="tb-form-table tb-layout-breakpoints">
<div class="tb-form-table-header no-padding-right">
<div class="tb-form-table-header-cell tb-icon"></div>
<div class="tb-form-table-header-cell tb-breakpoint" translate>layout.breakpoints</div>
<div class="tb-form-table-header-cell tb-size" translate>layout.size</div>
<div class="tb-form-table-header-cell tb-actions"></div>
</div>
<div class="tb-form-table-body no-gap">
@for (breakpoint of layoutBreakpoints; track breakpoint; let last = $last) {
<div class="tb-form-table-row no-padding-right">
<div class="tb-form-table-row-cell tb-icon">
<tb-icon>{{ breakpoint.icon }}</tb-icon>
</div>
<div class="tb-form-table-row-cell tb-breakpoint">
{{ breakpoint.name }}
</div>
<div class="tb-form-table-row-cell tb-size" tbTruncateWithTooltip>
{{ breakpoint.descriptionSize }}
</div>
<div class="tb-form-table-row-cell-buttons tb-actions">
<button mat-icon-button
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('main', breakpoint.breakpoint)"
type="button" >
<mat-icon>settings</mat-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
[disabled]="breakpoint.breakpoint === 'default'"
[class.tb-hidden]="breakpoint.breakpoint === 'default'"
(click)="deleteBreakpoint($event, breakpoint.breakpoint)"
type="button" >
<tb-icon>delete</tb-icon>
</button>
</div>
</div>
<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
type="button"
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('main', breakpoint.breakpoint)"
type="button" >
(click)="openLayoutSettings('right')"
class="tb-layout-preview-element"
aria-label="Layout settings"
[class]="layoutButtonClass('right')">
<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 type="button"
mat-raised-button
color="primary"
class="tb-layout-button tb-layout-button-right flex-1"
(mouseover)="rightLayoutTooltip.show()"
(mouseleave)="rightLayoutTooltip.hide()"
(click)="setFixedLayout('right')"
[class]="layoutButtonClass('right')">
<span [matTooltip]="layoutButtonText('right')"
#rightLayoutTooltip="matTooltip"
matTooltipPosition="above">
{{ 'layout.right' | translate }}
</span>
</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>
<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 class="flex flex-1 flex-col items-center justify-start" *ngIf="isDividerLayout" matTooltip="">
<button mat-icon-button
type="button"
matTooltip="{{ 'layout.settings' | translate }}"
matTooltipPosition="above"
(click)="openLayoutSettings('right')"
class="tb-layout-preview-element"
aria-label="Layout settings"
[class]="layoutButtonClass('right')">
<mat-icon>settings</mat-icon>
</button>
<button type="button"
mat-raised-button
color="primary"
class="tb-layout-button tb-layout-button-right flex-1"
(mouseover)="rightLayoutTooltip.show()"
(mouseleave)="rightLayoutTooltip.hide()"
(click)="setFixedLayout('right')"
[class]="layoutButtonClass('right')">
<span [matTooltip]="layoutButtonText('right')"
#rightLayoutTooltip="matTooltip"
matTooltipPosition="above">
{{ 'layout.right' | translate }}
</span>
</button>
<div class="tb-layout-preview-element tb-layout-preview-input flex-1" *ngIf="showPreviewInputs('right')">
<input *ngIf="layoutsFormGroup.get('type').value !== layoutWidthType.FIXED"
formControlName="rightWidthPercentage"
type="number"
step="1"
min="{{ layoutPercentageSize.MIN }}"
max="{{ layoutPercentageSize.MAX }}"
required>
<input *ngIf="layoutsFormGroup.get('type').value === layoutWidthType.FIXED"
formControlName="fixedWidth"
type="number"
step="1"
min="{{ layoutFixedSize.MIN }}"
max="{{ layoutFixedSize.MAX }}"
required>
@if (isDividerLayout) {
<div class="flex flex-col items-center justify-center gap-2" style="width: 80%">
@if (layoutsFormGroup.get('type').value === layoutWidthType.PERCENTAGE) {
<mat-slider
step="1"
min="{{ layoutPercentageSize.MIN }}"
max="{{ layoutPercentageSize.MAX }}"
style="width: 100%;"
color="accent"
discrete
[displayWith]="formatSliderTooltipLabel.bind(this)"><input matSliderThumb formControlName="sliderPercentage"/>
</mat-slider>
}
@if (layoutsFormGroup.get('type').value !== layoutWidthType.PERCENTAGE) {
<mat-slider
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>
</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>
</ng-template>
}
</section>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
<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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<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 class="tb-form-row space-between column-xs">
<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"
(ngModelChange)="selectLayoutChanged()">
<mat-select-trigger>{{ getName(selectedBreakpoint) }}</mat-select-trigger>
<mat-option *ngFor="let breakpointId of breakpointIds" [value]="breakpointId" style="min-height:56px;">
<mat-icon><tb-icon>{{ getIcon(breakpointId) }}</tb-icon></mat-icon>
<div>{{ getName(breakpointId) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpointId) }}</div>
</mat-option>
@for (breakpointId of breakpointIds; track breakpointId) {
<mat-option [value]="breakpointId" style="min-height:56px;">
<mat-icon><tb-icon>{{ getIcon(breakpointId) }}</tb-icon></mat-icon>
<div>{{ getName(breakpointId) }}</div>
<div style="font-size: 12px;letter-spacing: 0.4px;color: #9e9e9e;">{{ getSizeDescription(breakpointId) }}</div>
</mat-option>
}
</mat-select>

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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col">
<mat-form-field class="mat-block">
<mat-label translate>dashboard.state-name</mat-label>
<input required matInput formControlName="name">
<mat-error *ngIf="stateFormGroup.get('name').hasError('required')">
{{ 'dashboard.state-name-required' | translate }}
</mat-error>
@if (stateFormGroup.get('name').hasError('required')) {
<mat-error>
{{ 'dashboard.state-name-required' | translate }}
</mat-error>
}
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>dashboard.state-id</mat-label>
<input required matInput formControlName="id">
<mat-error *ngIf="stateFormGroup.get('id').hasError('required')">
{{ 'dashboard.state-id-required' | translate }}
</mat-error>
<mat-error *ngIf="stateFormGroup.get('id').hasError('stateExists')">
{{ 'dashboard.state-id-exists' | translate }}
</mat-error>
@if (stateFormGroup.get('id').hasError('required')) {
<mat-error>
{{ 'dashboard.state-id-required' | translate }}
</mat-error>
}
@if (stateFormGroup.get('id').hasError('stateExists')) {
<mat-error>
{{ 'dashboard.state-id-exists' | translate }}
</mat-error>
}
</mat-form-field>
<mat-checkbox formControlName="root">
{{ '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()"
[(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()">
<mat-option *ngFor="let stateKv of states | keyvalue" [value]="stateKv.key">
{{getStateName(stateKv.key, stateKv.value)}}
</mat-option>
[(ngModel)]="stateObject[0].id" (ngModelChange)="selectedStateIdChanged()">
@for (stateKv of states | keyvalue; track stateKv) {
<mat-option [value]="stateKv.key">
{{getStateName(stateKv.key, stateKv.value)}}
</mat-option>
}
</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 *ngIf="!isMobile || stateObject.length === 1" class="flex flex-row items-center justify-start">
<span *ngFor="let state of stateObject; index as i; last as isLast" class="state-entry"
@if (!isMobile || stateObject.length === 1) {
<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.cursor]="isLast ? 'default' : 'pointer'" (click)="navigatePrevState(i)">
{{getStateName(i)}}
{{getStateName(i)}}
<span class='state-divider' [class.!hidden]="isLast"> > </span>
</span>
</div>
<mat-select *ngIf="isMobile && stateObject.length > 1"
[(ngModel)]="selectedStateIndex" (ngModelChange)="selectedStateIndexChanged()">
<mat-option *ngFor="let state of stateObject; index as i" [value]="i">
{{getStateName(i)}}
</mat-option>
</mat-select>
}
</div>
}
@if (isMobile && stateObject.length > 1) {
<mat-select
[(ngModel)]="selectedStateIndex" (ngModelChange)="selectedStateIndexChanged()">
@for (state of stateObject; track state; let i = $index) {
<mat-option [value]="i">
{{getStateName(i)}}
</mat-option>
}
</mat-select>
}
</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">
<mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="type.display" *ngFor="let type of types"
(ngModelChange)="update()">
{{ 'widget.' + type.type | translate }}
</mat-checkbox>
@for (type of types; track type) {
<mat-checkbox style="padding-bottom: 8px;" [(ngModel)]="type.display"
(ngModelChange)="update()">
{{ 'widget.' + type.type | translate }}
</mat-checkbox>
}
</div>

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

@ -24,10 +24,10 @@
</div>
<div id="gridster-parent"
class="tb-dashboard-content flex-1" [class.autofill-height]="isAutofillHeight()"
[class.center-vertical]="centerVertical"
[class.center-horizontal]="centerHorizontal"
(mousedown)="onDashboardMouseDown($event)"
(tbcontextmenu)="openDashboardContextMenu($event)">
[class.center-vertical]="centerVertical"
[class.center-horizontal]="centerHorizontal"
(mousedown)="onDashboardMouseDown($event)"
(tbcontextmenu)="openDashboardContextMenu($event)">
<div #dashboardMenuTrigger="matMenuTrigger" style="visibility: hidden; position: fixed"
[style.left]="dashboardMenuPosition.x"
[style.top]="dashboardMenuPosition.y"
@ -36,13 +36,19 @@
<mat-menu #dashboardMenu="matMenu">
<ng-template matMenuContent let-items="items">
<div class="tb-dashboard-context-menu-items">
<button mat-menu-item *ngFor="let item of items;"
[disabled]="!item.enabled"
(click)="item.action(dashboardContextMenuEvent)">
<span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<mat-icon *ngIf="item.icon">{{item.icon}}</mat-icon>
<span translate>{{item.value}}</span>
</button>
@for (item of items; track item) {
<button mat-menu-item
[disabled]="!item.enabled"
(click)="item.action(dashboardContextMenuEvent)">
@if (item.shortcut) {
<span class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
}
@if (item.icon) {
<mat-icon>{{item.icon}}</mat-icon>
}
<span translate>{{item.value}}</span>
</button>
}
</div>
</ng-template>
</mat-menu>
@ -54,37 +60,45 @@
<mat-menu #widgetMenu="matMenu">
<ng-template matMenuContent let-items="items" let-widget="widget">
<div class="tb-dashboard-context-menu-items">
<button mat-menu-item *ngFor="let item of items;"
[disabled]="!item.enabled"
(click)="item.action(widgetContextMenuEvent, widget)">
<span *ngIf="item.shortcut" class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
<tb-icon matMenuItemIcon *ngIf="item.icon">{{item.icon}}</tb-icon>
<span translate>{{item.value}}</span>
</button>
@for (item of items; track item) {
<button mat-menu-item
[disabled]="!item.enabled"
(click)="item.action(widgetContextMenuEvent, widget)">
@if (item.shortcut) {
<span class="tb-alt-text"> {{ item.shortcut | keyboardShortcut }}</span>
}
@if (item.icon) {
<tb-icon matMenuItemIcon>{{item.icon}}</tb-icon>
}
<span translate>{{item.value}}</span>
</button>
}
</div>
</ng-template>
</mat-menu>
<div [class]="dashboardClass" id="gridster-background">
<gridster #gridster id="gridster-child" [options]="gridsterOpts">
<gridster-item #gridsterItem [item]="widget" [class]="{'tb-noselect': isEdit}" *ngFor="let widget of dashboardWidgets">
<tb-widget-container
[gridsterItem]="gridsterItem"
[widget]="widget"
[dashboardWidgets]="dashboardWidgets"
[dashboardStyle]="dashboardStyle"
[backgroundImage]="backgroundImage"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isPreview]="isPreview"
[isMobile]="isMobileSize"
[isEditActionEnabled]="isEditActionEnabled"
[isExportActionEnabled]="isExportActionEnabled"
[isRemoveActionEnabled]="isRemoveActionEnabled"
[disableWidgetInteraction]="disableWidgetInteraction"
(widgetFullscreenChanged)="onWidgetFullscreenChanged($event)"
(widgetComponentAction)="onWidgetComponentAction($event, widget)">
</tb-widget-container>
</gridster-item>
@for (widget of dashboardWidgets; track widget) {
<gridster-item #gridsterItem [item]="widget" [class]="{'tb-noselect': isEdit}">
<tb-widget-container
[gridsterItem]="gridsterItem"
[widget]="widget"
[dashboardWidgets]="dashboardWidgets"
[dashboardStyle]="dashboardStyle"
[backgroundImage]="backgroundImage"
[isEdit]="isEdit"
[isEditingWidget]="isEditingWidget"
[isPreview]="isPreview"
[isMobile]="isMobileSize"
[isEditActionEnabled]="isEditActionEnabled"
[isExportActionEnabled]="isExportActionEnabled"
[isRemoveActionEnabled]="isRemoveActionEnabled"
[disableWidgetInteraction]="disableWidgetInteraction"
(widgetFullscreenChanged)="onWidgetFullscreenChanged($event)"
(widgetComponentAction)="onWidgetComponentAction($event, widget)">
</tb-widget-container>
</gridster-item>
}
</gridster>
</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>
<span class="flex-1"></span>
<button mat-icon-button
(click)="cancel()"
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col 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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async" class="flex flex-col">
<mat-form-field class="mat-block">
<mat-label translate>dashboard.state</mat-label>
<mat-select required formControlName="stateId">
<mat-option *ngFor="let stateItem of states | keyvalue" [value]="stateItem.key">
{{ stateItem.value.name }}
</mat-option>
@for (stateItem of states | keyvalue; track stateItem) {
<mat-option [value]="stateItem.key">
{{ stateItem.value.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</fieldset>

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

@ -28,41 +28,49 @@
<ng-content select=".header-pane"></ng-content>
</span>
</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()">
<mat-icon class="material-icons">arrow_back</mat-icon>
<mat-icon class="material-icons">search</mat-icon>
</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>
<button *ngIf="showCloseDetails" mat-icon-button (click)="onCloseDetails()">
<mat-icon class="material-icons">close</mat-icon>
</button>
@if (showCloseDetails) {
<button mat-icon-button (click)="onCloseDetails()">
<mat-icon class="material-icons">close</mat-icon>
</button>
}
</div>
<section *ngIf="!isReadOnly" class="layout-wrap tb-header-buttons flex flex-row">
<button [disabled]="(isLoading$ | async) || theForm?.invalid || !theForm?.dirty"
mat-fab
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header mat-fab-bottom-right"
[class.tb-hide]="!isEdit"
(click)="onApplyDetails()">
<mat-icon class="material-icons">done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)"
mat-fab
matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode')) | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header mat-fab-bottom-right"
(click)="onToggleDetailsEditMode()">
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon>
</button>
</section>
@if (!isReadOnly) {
<section class="layout-wrap tb-header-buttons flex flex-row">
<button [disabled]="(isLoading$ | async) || theForm?.invalid || !theForm?.dirty"
mat-fab
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header mat-fab-bottom-right"
[class.tb-hide]="!isEdit"
(click)="onApplyDetails()">
<mat-icon class="material-icons">done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async) || (isAlwaysEdit && !theForm?.dirty)"
mat-fab
matTooltip="{{ (isAlwaysEdit ? 'action.decline-changes' : (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode')) | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header mat-fab-bottom-right"
(click)="onToggleDetailsEditMode()">
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon>
</button>
</section>
}
</mat-toolbar>
</header>
<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.
-->
<ng-container *ngIf="panelMode; else componentMode">
@if (panelMode) {
<ng-container *ngTemplateOutlet="deviceFilterPanel"></ng-container>
</ng-container>
<ng-template #componentMode>
<ng-container *ngIf="buttonMode; else deviceFilter">
} @else {
@if (buttonMode) {
<button color="primary"
matTooltip="{{buttonDisplayValue}}"
matTooltipPosition="above"
@ -27,8 +26,33 @@
(click)="toggleDeviceFilterPanel($event)">
<mat-icon>filter_list</mat-icon>{{buttonDisplayValue}}
</button>
</ng-container>
</ng-template>
} @else {
<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>
<form class="mat-content mat-padding flex flex-col" (ngSubmit)="update()">
<ng-container *ngTemplateOutlet="deviceFilter"></ng-container>
@ -59,13 +83,13 @@
<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">
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">

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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content>
<tb-anchor #entityDetailsForm></tb-anchor>
</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) } }}
</div>
</mat-slide-toggle>
<button mat-icon-button *ngIf="(isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched"
class="tb-mat-20"
matTooltip="{{ 'action.reset' | translate }}"
matTooltipPosition="above"
color="primary"
(click)="onReset()">
<mat-icon class="material-icons">refresh</mat-icon>
</button>
@if ((isDebugAllActive$ | async) && !allEnabled && debugAllControl.untouched) {
<button mat-icon-button
class="tb-mat-20"
matTooltip="{{ 'action.reset' | translate }}"
matTooltipPosition="above"
color="primary"
(click)="onReset()">
<mat-icon class="material-icons">refresh</mat-icon>
</button>
}
</div>
</div>
<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 *ngIf="entitiesTableConfig.detailsPanelEnabled"
class="tb-details-drawer mat-elevation-z4"
#drawer
mode="over"
position="end"
[opened]="isDetailsOpen">
<tb-entity-details-panel
#entityDetailsPanel
[entitiesTableConfig]="entitiesTableConfig"
[entityId]="isDetailsOpen ? dataSource.currentEntity?.id : null"
(closeEntityDetails)="isDetailsOpen = false; detailsPanelOpened.emit(isDetailsOpen);"
(entityUpdated)="onEntityUpdated($event)"
(entityAction)="onEntityAction($event)"
>
</tb-entity-details-panel>
</mat-drawer>
@if (entitiesTableConfig.detailsPanelEnabled) {
<mat-drawer
class="tb-details-drawer mat-elevation-z4"
#drawer
mode="over"
position="end"
[opened]="isDetailsOpen">
<tb-entity-details-panel
#entityDetailsPanel
[entitiesTableConfig]="entitiesTableConfig"
[entityId]="isDetailsOpen ? dataSource.currentEntity?.id : null"
(closeEntityDetails)="isDetailsOpen = false; detailsPanelOpened.emit(isDetailsOpen);"
(entityUpdated)="onEntityUpdated($event)"
(entityAction)="onEntityAction($event)"
>
</tb-entity-details-panel>
</mat-drawer>
}
<mat-drawer-content>
<div class="mat-padding tb-entity-table tb-absolute-fill">
<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()">
<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">
<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-timewindow *ngIf="entitiesTableConfig.useTimePageLink" [(ngModel)]="timewindow"
(ngModelChange)="onTimewindowChange()"
asButton strokedButton historyOnly [forAllTimeEnabled]="entitiesTableConfig.forAllTimeEnabled"></tb-timewindow>
@if (entitiesTableConfig.useTimePageLink) {
<tb-timewindow [(ngModel)]="timewindow"
(ngModelChange)="onTimewindowChange()"
asButton strokedButton historyOnly [forAllTimeEnabled]="entitiesTableConfig.forAllTimeEnabled"></tb-timewindow>
}
</div>
<span class="flex-1"></span>
<div [class.!hidden]="!addEnabled() || entitiesTableConfig.addAsTextButton">
<ng-container *ngIf="!entitiesTableConfig.addActionDescriptors.length; else iconCustom">
@if (!entitiesTableConfig.addActionDescriptors.length) {
<button mat-icon-button
[disabled]="isLoading$ | async"
(click)="addEntity($event)"
@ -54,48 +60,52 @@
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</ng-container>
<ng-template #iconCustom>
<ng-container *ngIf="entitiesTableConfig.addActionDescriptors.length === 1; else iconMenu">
<button *ngIf="entitiesTableConfig.addActionDescriptors[0].isEnabled()"
mat-icon-button
[disabled]="isLoading$ | async"
(click)="entitiesTableConfig.addActionDescriptors[0].onAction($event)"
matTooltip="{{ entitiesTableConfig.addActionDescriptors[0].name }}"
matTooltipPosition="above">
<tb-icon>{{entitiesTableConfig.addActionDescriptors[0].icon}}</tb-icon>
</button>
</ng-container>
<ng-template #iconMenu>
} @else {
@if (entitiesTableConfig.addActionDescriptors.length === 1) {
@if (entitiesTableConfig.addActionDescriptors[0].isEnabled()) {
<button
mat-icon-button
[disabled]="isLoading$ | async"
(click)="entitiesTableConfig.addActionDescriptors[0].onAction($event)"
matTooltip="{{ entitiesTableConfig.addActionDescriptors[0].name }}"
matTooltipPosition="above">
<tb-icon>{{entitiesTableConfig.addActionDescriptors[0].icon}}</tb-icon>
</button>
}
} @else {
<button mat-icon-button [disabled]="isLoading$ | async"
matTooltip="{{ translations.add | translate }}"
matTooltipPosition="above"
[matMenuTriggerFor]="addActionsMenu">
<mat-icon>add</mat-icon>
</button>
</ng-template>
</ng-template>
}
}
</div>
<button mat-icon-button [disabled]="isLoading$ | async"
[class.!hidden]="!actionDescriptor.isEnabled()" *ngFor="let actionDescriptor of headerActionDescriptors"
matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event)">
<tb-icon>{{actionDescriptor.icon}}</tb-icon>
</button>
@for (actionDescriptor of headerActionDescriptors; track actionDescriptor) {
<button mat-icon-button [disabled]="isLoading$ | async"
[class.!hidden]="!actionDescriptor.isEnabled()"
matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event)">
<tb-icon>{{actionDescriptor.icon}}</tb-icon>
</button>
}
<button mat-icon-button [disabled]="isLoading$ | async" (click)="updateData()"
matTooltip="{{ 'action.refresh' | translate }}"
matTooltipPosition="above">
<mat-icon>refresh</mat-icon>
</button>
<button *ngIf="entitiesTableConfig.searchEnabled"
mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
matTooltip="{{ translations.search | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
@if (entitiesTableConfig.searchEnabled) {
<button
mat-icon-button [disabled]="isLoading$ | async" (click)="enterFilterMode()"
matTooltip="{{ translations.search | translate }}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
}
<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"
mat-flat-button color="primary"
[disabled]="isLoading$ | async"
@ -111,10 +121,9 @@
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</ng-container>
<ng-template #textActions>
<ng-container *ngIf="entitiesTableConfig.addActionDescriptors.length === 1; else textMenu">
<ng-container *ngIf="entitiesTableConfig.addActionDescriptors[0].isEnabled()">
} @else {
@if (entitiesTableConfig.addActionDescriptors.length === 1) {
@if (entitiesTableConfig.addActionDescriptors[0].isEnabled()) {
<button class="lt-sm:!hidden"
mat-flat-button color="primary"
[disabled]="isLoading$ | async"
@ -130,9 +139,8 @@
matTooltipPosition="above">
<mat-icon>{{entitiesTableConfig.addActionDescriptors[0].icon}}</mat-icon>
</button>
</ng-container>
</ng-container>
<ng-template #textMenu>
}
} @else {
<button class="lt-sm:!hidden"
mat-flat-button color="primary"
[disabled]="isLoading$ | async"
@ -148,17 +156,19 @@
matTooltipPosition="above">
<mat-icon>add</mat-icon>
</button>
</ng-template>
</ng-template>
}
}
</div>
<mat-menu #addActionsMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let actionDescriptor of entitiesTableConfig.addActionDescriptors"
[disabled]="isLoading$ | async"
[class.!hidden]="!actionDescriptor.isEnabled()"
(click)="actionDescriptor.onAction($event)">
<tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
<span>{{ actionDescriptor.name }}</span>
</button>
@for (actionDescriptor of entitiesTableConfig.addActionDescriptors; track actionDescriptor) {
<button mat-menu-item
[disabled]="isLoading$ | async"
[class.!hidden]="!actionDescriptor.isEnabled()"
(click)="actionDescriptor.onAction($event)">
<tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
<span>{{ actionDescriptor.name }}</span>
</button>
}
</mat-menu>
</div>
</mat-toolbar>
@ -182,24 +192,28 @@
</button>
</div>
</mat-toolbar>
<mat-toolbar *ngIf="entitiesTableConfig.selectionEnabled" class="mat-mdc-table-toolbar" color="primary" [class.!hidden]="dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span class="tb-entity-table-info">
{{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }}
</span>
<span class="flex-1"></span>
<button mat-icon-button [disabled]="isLoading$ | async"
[class.!hidden]="!actionDescriptor.isEnabled" *ngFor="let actionDescriptor of groupActionDescriptors"
matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, dataSource.selection.selected)">
<tb-icon>{{actionDescriptor.icon}}</tb-icon>
</button>
</div>
</mat-toolbar>
@if (entitiesTableConfig.selectionEnabled) {
<mat-toolbar class="mat-mdc-table-toolbar" color="primary" [class.!hidden]="dataSource.selection.isEmpty()">
<div class="mat-toolbar-tools">
<span class="tb-entity-table-info">
{{ translations.selectedEntities | translate:{count: dataSource.selection.selected.length} }}
</span>
<span class="flex-1"></span>
@for (actionDescriptor of groupActionDescriptors; track actionDescriptor) {
<button mat-icon-button [disabled]="isLoading$ | async"
[class.!hidden]="!actionDescriptor.isEnabled"
matTooltip="{{ actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, dataSource.selection.selected)">
<tb-icon>{{actionDescriptor.icon}}</tb-icon>
</button>
}
</div>
</mat-toolbar>
}
<div class="table-container flex-1">
<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>
<mat-header-cell *matHeaderCellDef style="width: 40px;">
<mat-checkbox (change)="$event ? dataSource.masterToggle() : null"
@ -215,82 +229,90 @@
</mat-checkbox>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="column.key" *ngFor="let column of entityColumns; trackBy: trackByColumnKey;">
<mat-header-cell [class.mat-number-cell]="column.isNumberColumn"
[class.lt-lg:!hidden]="column.mobileHide"
*matHeaderCellDef [style]="headerCellStyle(column)" mat-sort-header [disabled]="!column.sortable">
@for (column of entityColumns; track column.key) {
<ng-container [matColumnDef]="column.key">
<mat-header-cell [class.mat-number-cell]="column.isNumberColumn"
[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>
<mat-cell [class.mat-number-cell]="column.isNumberColumn"
[class.cell-action]="column.actionCell?.type === cellActionType.COPY_BUTTON"
[class.lt-lg:!hidden]="column.mobileHide"
*matCellDef="let entity; let row = index"
[matTooltip]="cellTooltip(entity, column, row)"
#cellMatTooltip="matTooltip"
matTooltipPosition="above"
[style]="cellStyle(entity, column, row)">
<ng-container [ngSwitch]="column.type">
<ng-template [ngSwitchCase]="'link'">
<a *ngIf="column.entityURL(entity) as detailsPageURL; else defaultContent"
[routerLink]="detailsPageURL" [innerHTML]="cellContent(entity, column, row)"
(click)="$event.stopPropagation();"></a>
</ng-template>
<ng-template [ngSwitchCase]="'entityChips'">
<tb-entity-chips [entity]="entity" [key]="column.key"
[detailsPagePrefixUrl]="column.entityURL ? column.entityURL(entity) : ''"></tb-entity-chips>
</ng-template>
<ng-template #defaultContent ngSwitchDefault>
<span [innerHTML]="cellContent(entity, column, row)"></span>
</ng-template>
</ng-container>
<ng-template [ngIf]="column.actionCell">
<ng-container [ngSwitch]="column.actionCell.type">
<ng-template [ngSwitchCase]="cellActionType.COPY_BUTTON">
<tb-copy-button
class="copy-button"
[disabled]="isLoading$ | async"
[class.!hidden]="!column.actionCell.isEnabled(entity)"
[copyText]="column.actionCell.onAction(null, entity)"
tooltipText="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
tooltipPosition="above"
(mouseover)="cellMatTooltip.hide()"
(mouseleave)="cellMatTooltip.show()"
[icon]="column.actionCell.icon"
[style]="column.actionCell.style">
</tb-copy-button>
</ng-template>
<ng-template ngSwitchDefault>
<button mat-icon-button [disabled]="isLoading$ | async"
[class.!hidden]="!column.actionCell.isEnabled(entity)"
matTooltip="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
matTooltipPosition="above"
(click)="column.actionCell.onAction($event, entity)">
<tb-icon [style]="column.actionCell.style">
{{column.actionCell.icon}}
</tb-icon>
</button>
</ng-template>
</ng-container>
</ng-template>
</mat-cell>
</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"
[style]="cellStyle(entity, column, row)">
<button mat-icon-button [disabled]="isLoading$ | async"
[class.!hidden]="!column.actionDescriptor.isEnabled(entity)"
matTooltip="{{ column.actionDescriptor.nameFunction ? column.actionDescriptor.nameFunction(entity) : column.actionDescriptor.name }}"
matTooltipPosition="above"
(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>
<mat-cell [class.mat-number-cell]="column.isNumberColumn"
[class.cell-action]="column.actionCell?.type === cellActionType.COPY_BUTTON"
[class.lt-lg:!hidden]="column.mobileHide"
*matCellDef="let entity; let row = index"
[matTooltip]="cellTooltip(entity, column, row)"
#cellMatTooltip="matTooltip"
matTooltipPosition="above"
[style]="cellStyle(entity, column, row)">
@switch (column.type) {
@case ('link') {
@if (column.entityURL(entity); as detailsPageURL) {
<a
[routerLink]="detailsPageURL" [innerHTML]="cellContent(entity, column, row)"
(click)="$event.stopPropagation();"></a>
} @else {
<span [innerHTML]="cellContent(entity, column, row)"></span>
}
}
@case ('entityChips') {
<tb-entity-chips [entity]="entity" [key]="column.key"
[detailsPagePrefixUrl]="column.entityURL ? column.entityURL(entity) : ''"></tb-entity-chips>
}
@default {
<span [innerHTML]="cellContent(entity, column, row)"></span>
}
}
@if (column.actionCell) {
@switch (column.actionCell.type) {
@case (cellActionType.COPY_BUTTON) {
<tb-copy-button
class="copy-button"
[disabled]="isLoading$ | async"
[class.!hidden]="!column.actionCell.isEnabled(entity)"
[copyText]="column.actionCell.onAction(null, entity)"
tooltipText="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
tooltipPosition="above"
(mouseover)="cellMatTooltip.hide()"
(mouseleave)="cellMatTooltip.show()"
[icon]="column.actionCell.icon"
[style]="column.actionCell.style">
</tb-copy-button>
}
@default {
<button mat-icon-button [disabled]="isLoading$ | async"
[class.!hidden]="!column.actionCell.isEnabled(entity)"
matTooltip="{{ column.actionCell.nameFunction ? column.actionCell.nameFunction(entity) : column.actionCell.name }}"
matTooltipPosition="above"
(click)="column.actionCell.onAction($event, entity)">
<tb-icon [style]="column.actionCell.style">
{{column.actionCell.icon}}
</tb-icon>
</button>
}
}
}
</mat-cell>
</ng-container>
}
@for (column of actionColumns; track trackByColumnKey($index, column)) {
<ng-container [matColumnDef]="column.key">
<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"
[style]="cellStyle(entity, column, row)">
<button mat-icon-button [disabled]="isLoading$ | async"
[class.!hidden]="!column.actionDescriptor.isEnabled(entity)"
matTooltip="{{ column.actionDescriptor.nameFunction ? column.actionDescriptor.nameFunction(entity) : column.actionDescriptor.name }}"
matTooltipPosition="above"
(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>
<mat-header-cell *matHeaderCellDef>
<ng-container *ngIf="cellActionDescriptors.length">
@if (cellActionDescriptors.length) {
<div class="gt-md:!hidden" style="min-width: 48px;">
{{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
</div>
@ -298,21 +320,22 @@
[style.min-width]="(cellActionDescriptors.length * 48) + 'px'">
{{ entitiesTableConfig.actionsColumnTitle ? (entitiesTableConfig.actionsColumnTitle | translate) : '' }}
</div>
</ng-container>
}
</mat-header-cell>
<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"
[style.min-width]="(cellActionDescriptors.length * 48) + 'px'">
<button mat-icon-button [disabled]="(isLoading$ | async) || !actionDescriptor.isEnabled(entity)"
*ngFor="let actionDescriptor of cellActionDescriptors"
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, entity)">
<tb-icon [style]="actionDescriptor.style">
{{actionDescriptor.iconFunction ? actionDescriptor.iconFunction(entity) : actionDescriptor.icon}}
</tb-icon>
</button>
@for (actionDescriptor of cellActionDescriptors; track actionDescriptor) {
<button mat-icon-button [disabled]="(isLoading$ | async) || !actionDescriptor.isEnabled(entity)"
matTooltip="{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}"
matTooltipPosition="above"
(click)="actionDescriptor.onAction($event, entity)">
<tb-icon [style]="actionDescriptor.style">
{{actionDescriptor.iconFunction ? actionDescriptor.iconFunction(entity) : actionDescriptor.icon}}
</tb-icon>
</button>
}
</div>
<div [class.!hidden]="cellActionDescriptors.length === 1" class="gt-md:!hidden">
<button mat-icon-button
@ -321,18 +344,20 @@
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-menu-item *ngFor="let actionDescriptor of cellActionDescriptors"
[disabled]="isLoading$ | async"
[class.!hidden]="!actionDescriptor.isEnabled(entity)"
(click)="actionDescriptor.onAction($event, entity)">
<tb-icon matMenuItemIcon [style]="actionDescriptor.style">
{{actionDescriptor.iconFunction ? actionDescriptor.iconFunction(entity) : actionDescriptor.icon}}
</tb-icon>
<span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span>
</button>
@for (actionDescriptor of cellActionDescriptors; track actionDescriptor) {
<button mat-menu-item
[disabled]="isLoading$ | async"
[class.!hidden]="!actionDescriptor.isEnabled(entity)"
(click)="actionDescriptor.onAction($event, entity)">
<tb-icon matMenuItemIcon [style]="actionDescriptor.style">
{{actionDescriptor.iconFunction ? actionDescriptor.iconFunction(entity) : actionDescriptor.icon}}
</tb-icon>
<span>{{ actionDescriptor.nameFunction ? actionDescriptor.nameFunction(entity) : actionDescriptor.name }}</span>
</button>
}
</mat-menu>
</div>
</ng-container>
}
</mat-cell>
</ng-container>
<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.tb-current-entity]="dataSource.isCurrentEntity(entity)"
[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>
<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"
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>
<mat-divider *ngIf="displayPagination"></mat-divider>
<mat-paginator *ngIf="displayPagination"
[length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator>
@if (displayPagination) {
<mat-divider></mat-divider>
}
@if (displayPagination) {
<mat-paginator
[length]="dataSource.total() | async"
[pageIndex]="pageLink.page"
[pageSize]="pageLink.pageSize"
[pageSizeOptions]="pageSizeOptions"
[hidePageSize]="hidePageSize"
showFirstLastButtons></mat-paginator>
}
</div>
</div>
</mat-drawer-content>

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

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

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

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

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

@ -30,25 +30,27 @@
</div>
<div class="tb-help" [tb-help]="helpLinkId()"></div>
<span class="flex-1"></span>
<section *ngIf="!isReadOnly" class="tb-header-button flex flex-row gap-2">
<button [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty"
mat-fab
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header"
[class.tb-hide]="!isEdit"
(click)="onApplyDetails()">
<mat-icon class="material-icons">done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async)"
mat-fab
matTooltip="{{ (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header"
(click)="onToggleDetailsEditMode()">
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon>
</button>
</section>
@if (!isReadOnly) {
<section class="tb-header-button flex flex-row gap-2">
<button [disabled]="(isLoading$ | async) || detailsForm.invalid || !detailsForm.dirty"
mat-fab
matTooltip="{{ 'action.apply-changes' | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header"
[class.tb-hide]="!isEdit"
(click)="onApplyDetails()">
<mat-icon class="material-icons">done</mat-icon>
</button>
<button [disabled]="(isLoading$ | async)"
mat-fab
matTooltip="{{ (isEdit ? 'action.decline-changes' : 'details.toggle-edit-mode') | translate }}"
matTooltipPosition="above"
color="accent" class="tb-btn-header"
(click)="onToggleDetailsEditMode()">
<mat-icon class="material-icons">{{isEdit ? 'close' : 'edit'}}</mat-icon>
</button>
</section>
}
</div>
</mat-toolbar>
<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 *ngIf="!filter || !filter.type; else filterView" class="entity-filter-empty" translate>alias.no-entity-filter-specified</div>
<ng-template #filterView>
@if (!filter || !filter.type) {
<div class="entity-filter-empty" translate>alias.no-entity-filter-specified</div>
} @else {
<div class="flex flex-col">
<div class="entity-filter-value">{{ filterDisplayValue }}</div>
</div>
</ng-template>
}
</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-label translate>alias.filter-type</mat-label>
<mat-select required formControlName="type">
<mat-option *ngFor="let type of aliasFilterTypes" [value]="type">
{{aliasFilterTypeTranslations.get(type) | translate}}
</mat-option>
@for (type of aliasFilterTypes; track type) {
<mat-option [value]="type">
{{aliasFilterTypeTranslations.get(type) | translate}}
</mat-option>
}
</mat-select>
<mat-error *ngIf="entityFilterFormGroup.get('type').hasError('required')">
{{ 'alias.filter-type-required' | translate }}
</mat-error>
@if (entityFilterFormGroup.get('type').hasError('required')) {
<mat-error>
{{ 'alias.filter-type-required' | translate }}
</mat-error>
}
</mat-form-field>
<section class="flex flex-col" [formGroup]="filterFormGroup" [ngSwitch]="entityFilterFormGroup.get('type').value">
<ng-template [ngSwitchCase]="aliasFilterType.singleEntity">
<tb-entity-select required
appearance="outline"
useAliasEntityTypes="true"
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"
<section class="flex flex-col" [formGroup]="filterFormGroup">
@switch (entityFilterFormGroup.get('type').value) {
@case (aliasFilterType.singleEntity) {
<tb-entity-select required
appearance="outline"
useAliasEntityTypes="true"
formControlName="defaultStateEntity">
formControlName="singleEntity">
</tb-entity-select>
</div>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.assetType">
<tb-entity-subtype-list required
floatLabel="always"
appearance="outline"
label="{{ 'asset.asset-types' | translate }}"
[entityType]="entityType.ASSET"
formControlName="assetTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>asset.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{'asset.help-text' | translate }}">help</mat-icon>
<input matInput formControlName="assetNameFilter">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.deviceType">
<tb-entity-subtype-list required
floatLabel="always"
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>
</ng-template>
<ng-template [ngSwitchCase]="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>
</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>
}
@case (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>
}
@case (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>
@if (filterFormGroup.get('entityNameFilter').hasError('required')) {
<mat-error>
{{ 'entity.entity-name-filter-required' | translate }}
</mat-error>
}
</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 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>
}
@case (aliasFilterType.assetType) {
<tb-entity-subtype-list required
floatLabel="always"
appearance="outline"
label="{{ 'asset.asset-types' | translate }}"
[entityType]="entityType.ASSET"
formControlName="assetTypes">
</tb-entity-subtype-list>
<mat-form-field class="mat-block" appearance="outline">
<mat-label translate>asset.name-starts-with</mat-label>
<mat-icon class="mat-primary" aria-hidden="false" aria-label="help-icon" matSuffix style="cursor:pointer;"
matTooltip="{{'asset.help-text' | translate }}">help</mat-icon>
<input matInput formControlName="assetNameFilter">
</mat-form-field>
}
@case (aliasFilterType.deviceType) {
<tb-entity-subtype-list required
floatLabel="always"
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>
<mat-slide-toggle *ngIf="filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value"
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>
</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 }}">
<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>
<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>
<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>
@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>
} @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>
} @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">
<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>
@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>
<mat-slide-toggle *ngIf="filterFormGroup.get('maxLevel').value > 1 || !filterFormGroup.get('maxLevel').value"
class="mat-slide" formControlName="fetchLastLevelOnly">
{{ 'alias.last-level-relation' | translate }}
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked no-padding-bottom">
<div class="tb-form-panel-title" translate>relation.relation-filter</div>
<tb-relation-type-autocomplete
appearance="outline"
subscriptSizing="dynamic"
formControlName="relationType">
</tb-relation-type-autocomplete>
<ng-container [ngSwitch]="entityFilterFormGroup.get('type').value">
<ng-template [ngSwitchCase]="aliasFilterType.assetSearchQuery">
<tb-entity-subtype-list
required
floatLabel="always"
appearance="outline"
label="{{ 'asset.asset-types' | translate }}"
[entityType]="entityType.ASSET"
formControlName="assetTypes">
</tb-entity-subtype-list>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.deviceSearchQuery">
<tb-entity-subtype-list
required
floatLabel="always"
appearance="outline"
label="{{ 'device.device-types' | translate }}"
[entityType]="entityType.DEVICE"
formControlName="deviceTypes">
</tb-entity-subtype-list>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.edgeSearchQuery">
<tb-entity-subtype-list
required
floatLabel="always"
appearance="outline"
label="{{ 'edge.edge-types' | translate }}"
[entityType]="entityType.EDGE"
formControlName="edgeTypes">
</tb-entity-subtype-list>
</ng-template>
<ng-template [ngSwitchCase]="aliasFilterType.entityViewSearchQuery">
<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>
</ng-template>
</ng-container>
</div>
</section>
</ng-template>
<div class="tb-form-panel stroked no-padding-bottom">
<div class="tb-form-panel-title" translate>relation.relation-filter</div>
<tb-relation-type-autocomplete
appearance="outline"
subscriptSizing="dynamic"
formControlName="relationType">
</tb-relation-type-autocomplete>
@switch (entityFilterFormGroup.get('type').value) {
@case (aliasFilterType.assetSearchQuery) {
<tb-entity-subtype-list
required
floatLabel="always"
appearance="outline"
label="{{ 'asset.asset-types' | translate }}"
[entityType]="entityType.ASSET"
formControlName="assetTypes">
</tb-entity-subtype-list>
}
@case (aliasFilterType.deviceSearchQuery) {
<tb-entity-subtype-list
required
floatLabel="always"
appearance="outline"
label="{{ 'device.device-types' | translate }}"
[entityType]="entityType.DEVICE"
formControlName="deviceTypes">
</tb-entity-subtype-list>
}
@case (aliasFilterType.edgeSearchQuery) {
<tb-entity-subtype-list
required
floatLabel="always"
appearance="outline"
label="{{ 'edge.edge-types' | translate }}"
[entityType]="entityType.EDGE"
formControlName="edgeTypes">
</tb-entity-subtype-list>
}
@case (aliasFilterType.entityViewSearchQuery) {
<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>
}
}
</div>
</section>
}
}
</section>
</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()">
<div class="tb-filter-container mat-padding flex flex-col">
<ng-container *ngFor="let column of showColumns">
<ng-container [ngSwitch]="column.key">
<ng-template [ngSwitchCase]="isSelector(column.key)">
@for (column of showColumns; track column) {
@switch (column.key) {
@case (isSelector(column.key)) {
<mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label>
<mat-select [formControlName]="column.key">
<mat-option [value]="">{{ 'event.all-events' | translate}}</mat-option>
<mat-option *ngFor="let value of selectorValues(column.key)" [value]="value">
{{ value }}
</mat-option>
@for (value of selectorValues(column.key); track value) {
<mat-option [value]="value">
{{ value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="isNumberFields(column.key)">
}
@case (isNumberFields(column.key)) {
<mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="number" min="0" [name]="column.key" [formControlName]="column.key">
<mat-error *ngIf="eventFilterFormGroup.get(column.key).hasError('min')">
{{ 'event.min-value' | translate }}
</mat-error>
@if (eventFilterFormGroup.get(column.key).hasError('min')) {
<mat-error>
{{ 'event.min-value' | translate }}
</mat-error>
}
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="'isError'">
}
@case ('isError') {
<tb-checkbox formControlName="isError" [falseValue]="''"
(ngModelChange)="changeIsError($event)">
{{ 'event.has-error' | translate }}
</tb-checkbox>
</ng-template>
<ng-template [ngSwitchCase]="'errorStr'">
}
@case ('errorStr') {
<mat-form-field [class.!hidden]="!showErrorMsgFields()">
<mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="text" name="errorSearchText" formControlName="errorStr">
</mat-form-field>
</ng-template>
<ng-container *ngSwitchDefault>
}
@default {
<mat-form-field>
<mat-label>{{ column.title | translate}}</mat-label>
<input matInput type="text" [name]="column.key" [formControlName]="column.key">
</mat-form-field>
</ng-container>
</ng-container>
</ng-container>
}
}
}
</div>
<div class="tb-panel-actions mat-padding flex flex-row items-center justify-end">
<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-select [ngModel]="eventTableConfig.eventType"
(ngModelChange)="eventTypeChanged($event)">
<mat-option *ngFor="let type of eventTableConfig.eventTypes" [value]="type">
{{ eventTypeTranslationsMap.get(type) | translate }}
</mat-option>
@for (type of eventTableConfig.eventTypes; track type) {
<mat-option [value]="type">
{{ eventTypeTranslationsMap.get(type) | translate }}
</mat-option>
}
</mat-select>
</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">
<mat-form-field hideRequiredMarker class="mat-block max-w-30% flex-full">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of booleanOperations" [value]="operation">
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
</mat-option>
@for (operation of booleanOperations; track operation) {
<mat-option [value]="operation">
{{booleanOperationTranslations.get(booleanOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
<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-label translate>filter.operation.operation</mat-label>
<mat-select required formControlName="operation">
<mat-option *ngFor="let operation of complexOperations" [value]="operation">
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
</mat-option>
@for (operation of complexOperations; track operation) {
<mat-option [value]="operation">
{{complexOperationTranslations.get(complexOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
<tb-filter-predicate-list
@ -54,11 +56,12 @@
cdkFocusInitial>
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!data.readonly"
type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
@if (!data.readonly) {
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || complexFilterFormGroup.invalid || !complexFilterFormGroup.dirty">
{{ (isAdd ? 'action.add' : 'action.update') | translate }}
</button>
}
</div>
</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 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 *ngIf="valueType === valueTypeEnum.STRING"
translate class="tb-title no-padding" style="min-width: 70px;">filter.ignore-case</label>
@if (valueType === valueTypeEnum.STRING) {
<label
translate class="tb-title no-padding" style="min-width: 70px;">filter.ignore-case</label>
}
</div>
<label translate class="tb-title no-padding max-w-70% flex-full">filter.value</label>
</div>
<label *ngIf="displayUserParameters"
translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label>
@if (displayUserParameters) {
<label
translate class="tb-title no-padding" style="width: 60px;">filter.user-parameters</label>
}
<span [class.!hidden]="disabled" style="min-width: 40px;">&nbsp;</span>
</div>
</div>
<mat-divider style="padding-bottom: 5px;"></mat-divider>
<div class="predicate-list">
<div class="flex flex-row items-stretch justify-start h-full min-h-19 max-h-24"
formArrayName="predicates"
*ngFor="let predicateControl of predicatesFormArray.controls; let $index = index">
<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>
<div class="flex max-w-92% min-w-147.5 flex-full flex-col">
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<tb-filter-predicate
class="flex-1"
[valueType]="valueType"
[displayUserParameters]="displayUserParameters"
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
[key]="key"
[formControl]="predicateControl">
</tb-filter-predicate>
<button mat-icon-button color="primary"
[class.!hidden]="disabled"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
@for (predicateControl of predicatesFormArray.controls; track predicateControl) {
<div class="flex flex-row items-stretch justify-start h-full min-h-19 max-h-24"
formArrayName="predicates"
>
<div class="filters-operation flex max-w-8% flex-full flex-row items-stretch justify-center">
@if ($index > 0) {
<span>{{ complexOperationTranslations.get(operation) | translate }}</span>
}
</div>
<div class="flex max-w-92% min-w-147.5 flex-full flex-col">
<div class="flex flex-1 flex-row items-center justify-start gap-2">
<tb-filter-predicate
class="flex-1"
[valueType]="valueType"
[displayUserParameters]="displayUserParameters"
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
[key]="key"
[formControl]="predicateControl">
</tb-filter-predicate>
<button mat-icon-button color="primary"
[class.!hidden]="disabled"
type="button"
(click)="removePredicate($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
</div>
}
<span [class.!hidden]="!!predicatesFormArray.length"
[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 style="margin-top: 16px;" class="flex flex-row gap-2">
<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-col" [class.!hidden]="dynamicMode || onlyUserDynamicSource">
<div class="flex flex-col" [ngSwitch]="valueType">
<ng-template [ngSwitchCase]="valueTypeEnum.STRING">
<mat-form-field hideRequiredMarker class="mat-block">
<input matInput formControlName="defaultValue" placeholder="{{'filter.value' | translate}}">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.NUMERIC">
<mat-form-field hideRequiredMarker class="mat-block">
<input required type="number" matInput formControlName="defaultValue"
placeholder="{{'filter.value' | translate}}">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.DATE_TIME">
<tb-datetime formControlName="defaultValue"
dateText="{{ 'filter.date' | translate }}"
required [showLabel]="false"></tb-datetime>
</ng-template>
<ng-template [ngSwitchCase]="valueTypeEnum.BOOLEAN">
<mat-checkbox formControlName="defaultValue">
{{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
</ng-template>
<div class="flex flex-col">
@switch (valueType) {
@case (valueTypeEnum.STRING) {
<mat-form-field hideRequiredMarker class="mat-block">
<input matInput formControlName="defaultValue" placeholder="{{'filter.value' | translate}}">
</mat-form-field>
}
@case (valueTypeEnum.NUMERIC) {
<mat-form-field hideRequiredMarker class="mat-block">
<input required type="number" matInput formControlName="defaultValue"
placeholder="{{'filter.value' | translate}}">
</mat-form-field>
}
@case (valueTypeEnum.DATE_TIME) {
<tb-datetime formControlName="defaultValue"
dateText="{{ 'filter.date' | translate }}"
required [showLabel]="false"></tb-datetime>
}
@case (valueTypeEnum.BOOLEAN) {
<mat-checkbox formControlName="defaultValue">
{{ (filterPredicateValueFormGroup.get('defaultValue').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
}
}
</div>
<div class="tb-hint">{{ hintText | translate }}</div>
</div>
@ -50,9 +52,11 @@
<mat-option [value]="null">
{{'filter.no-dynamic-value' | translate}}
</mat-option>
<mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType">
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
@for (sourceType of dynamicValueSourceTypes; track sourceType) {
<mat-option [value]="sourceType">
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
<div class="tb-hint" translate>filter.dynamic-source-type</div>
@ -63,23 +67,27 @@
</mat-form-field>
<div class="tb-hint" translate>filter.source-attribute</div>
</div>
<div *ngIf="!allow && inheritMode"
class="flex flex-col"
style="padding-top: 6px">
<mat-checkbox formControlName="inherit">
{{ 'filter.inherit-owner' | translate}}
</mat-checkbox>
<div class="tb-hint" translate>filter.source-attribute-not-set</div>
</div>
@if (!allow && inheritMode) {
<div
class="flex flex-col"
style="padding-top: 6px">
<mat-checkbox formControlName="inherit">
{{ 'filter.inherit-owner' | translate}}
</mat-checkbox>
<div class="tb-hint" translate>filter.source-attribute-not-set</div>
</div>
}
</div>
</div>
<button mat-icon-button *ngIf="!onlyUserDynamicSource"
class="mat-elevation-z1 tb-mat-32"
color="primary"
type="button"
matTooltip="{{ (dynamicMode ? 'filter.switch-to-default-value' : 'filter.switch-to-dynamic-value') | translate }}"
matTooltipPosition="above"
(click)="dynamicMode = !dynamicMode">
<tb-icon class="tb-mat-20">{{ dynamicMode ? 'mdi:numeric' : 'mdi:variable' }}</tb-icon>
</button>
@if (!onlyUserDynamicSource) {
<button mat-icon-button
class="mat-elevation-z1 tb-mat-32"
color="primary"
type="button"
matTooltip="{{ (dynamicMode ? 'filter.switch-to-default-value' : 'filter.switch-to-dynamic-value') | translate }}"
matTooltipPosition="above"
(click)="dynamicMode = !dynamicMode">
<tb-icon class="tb-mat-20">{{ dynamicMode ? 'mdi:numeric' : 'mdi:variable' }}</tb-icon>
</button>
}
</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-col" [ngSwitch]="type">
<ng-template [ngSwitchCase]="filterPredicateType.STRING">
<tb-string-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
formControlName="predicate">
</tb-string-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.NUMERIC">
<tb-numeric-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
[valueType]="valueType"
formControlName="predicate">
</tb-numeric-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.BOOLEAN">
<tb-boolean-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
formControlName="predicate">
</tb-boolean-filter-predicate>
</ng-template>
<ng-template [ngSwitchCase]="filterPredicateType.COMPLEX">
<tb-complex-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
[key]="key"
[valueType]="valueType"
[displayUserParameters]="displayUserParameters"
formControlName="predicate">
</tb-complex-filter-predicate>
</ng-template>
<div class="flex flex-1 flex-col">
@switch (type) {
@case (filterPredicateType.STRING) {
<tb-string-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
formControlName="predicate">
</tb-string-filter-predicate>
}
@case (filterPredicateType.NUMERIC) {
<tb-numeric-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
[valueType]="valueType"
formControlName="predicate">
</tb-numeric-filter-predicate>
}
@case (filterPredicateType.BOOLEAN) {
<tb-boolean-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
formControlName="predicate">
</tb-boolean-filter-predicate>
}
@case (filterPredicateType.COMPLEX) {
<tb-complex-filter-predicate
[allowUserDynamicSource]="allowUserDynamicSource"
[onlyUserDynamicSource]="onlyUserDynamicSource"
[key]="key"
[valueType]="valueType"
[displayUserParameters]="displayUserParameters"
formControlName="predicate">
</tb-complex-filter-predicate>
}
}
</div>
<tb-filter-user-info *ngIf="type !== filterPredicateType.COMPLEX && displayUserParameters"
style="width: 60px;"
class="flex flex-row items-stretch justify-center"
[valueType]="valueType"
[operation]="filterPredicateFormGroup.get('predicate').value?.operation"
[key]="key"
formControlName="userInfo">
</tb-filter-user-info>
@if (type !== filterPredicateType.COMPLEX && displayUserParameters) {
<tb-filter-user-info
style="width: 60px;"
class="flex flex-row items-stretch justify-center"
[valueType]="valueType"
[operation]="filterPredicateFormGroup.get('predicate').value?.operation"
[key]="key"
formControlName="userInfo">
</tb-filter-user-info>
}
</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">
</mat-form-field>
</div>
<div class="tb-form-row space-between" *ngIf="showUniInput">
<div class="min-w-44" translate>filter.unit</div>
<tb-unit-input supportsUnitConversion formControlName="unit" class="medium-width flex-xs">
</tb-unit-input>
</div>
@if (showUniInput) {
<div class="tb-form-row space-between">
<div class="min-w-44" translate>filter.unit</div>
<tb-unit-input supportsUnitConversion formControlName="unit" class="medium-width flex-xs">
</tb-unit-input>
</div>
}
</div>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
@ -65,11 +67,12 @@
cdkFocusInitial>
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!data.readonly"
type="submit"
[disabled]="(isLoading$ | async) || filterUserInfoFormGroup.invalid || !filterUserInfoFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
@if (!data.readonly) {
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || filterUserInfoFormGroup.invalid || !filterUserInfoFormGroup.dirty">
{{ 'action.update' | translate }}
</button>
}
</div>
</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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="(isLoading$ | async) === false"></div>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
@if ((isLoading$ | async) === false) {
<div style="height: 4px;"></div>
}
<div mat-dialog-content>
<div class="tb-filters-header flex flex-row items-center justify-start">
<span class="max-w-5% flex-full"></span>
<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 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>
</div>
<fieldset [disabled]="isLoading$ | async">
<mat-divider></mat-divider>
<div class="flex flex-1 flex-row items-center justify-start"
formArrayName="filters"
*ngFor="let filterControl of filtersFormArray().controls; let $index = index">
<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">
<mat-label class="flex-1">{{filterControl.get('filter').value}}</mat-label>
<section style="padding-left: 10px;"
class="tb-editable-switch flex min-w-20 max-w-20 flex-1 basis-20 flex-col items-center justify-center">
<mat-slide-toggle class="editable-switch"
[formControl]="filterControl.get('editable')">
</mat-slide-toggle>
</section>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="editFilter($index)"
matTooltip="{{ 'filter.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 48px;"
type="button"
(click)="duplicateFilter($index)"
matTooltip="{{ 'filter.duplicate-filter-action' | translate }}"
matTooltipPosition="above">
<mat-icon>content_copy</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="removeFilter($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
@for (filterControl of filtersFormArray().controls; track filterControl) {
<div class="flex flex-1 flex-row items-center justify-start"
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">
<mat-label class="flex-1">{{filterControl.get('filter').value}}</mat-label>
<section style="padding-left: 10px;"
class="tb-editable-switch flex min-w-20 max-w-20 flex-1 basis-20 flex-col items-center justify-center">
<mat-slide-toggle class="editable-switch"
[formControl]="filterControl.get('editable')">
</mat-slide-toggle>
</section>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="editFilter($index)"
matTooltip="{{ 'filter.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 48px;"
type="button"
(click)="duplicateFilter($index)"
matTooltip="{{ 'filter.duplicate-filter-action' | translate }}"
matTooltipPosition="above">
<mat-icon>content_copy</mat-icon>
</button>
<button [disabled]="isLoading$ | async"
mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="removeFilter($index)"
matTooltip="{{ 'filter.remove-filter' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
}
</fieldset>
</div>
<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 *ngFor="let filter of filtersInfo | keyvalue; let last = last">
<div class="flex flex-row items-center justify-start">
<mat-label class="flex-1">{{filter.value.filter}}</mat-label>
<button mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="editFilter(filter.key, filter.value)"
matTooltip="{{ 'filter.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
@for (filter of filtersInfo | keyvalue; track filter; let last = $last) {
<div>
<div class="flex flex-row items-center justify-start">
<mat-label class="flex-1">{{filter.value.filter}}</mat-label>
<button mat-icon-button color="primary"
style="min-width: 40px;"
type="button"
(click)="editFilter(filter.key, filter.value)"
matTooltip="{{ 'filter.edit' | translate }}"
matTooltipPosition="above">
<mat-icon>edit</mat-icon>
</button>
</div>
@if (!last) {
<mat-divider></mat-divider>
}
</div>
<mat-divider *ngIf="!last"></mat-divider>
</div>
}
</div>

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

@ -15,18 +15,20 @@
limitations under the License.
-->
<section class="tb-filters-edit flex flex-row items-center justify-start" *ngIf="hasEditableFilters">
<button mat-icon-button
cdkOverlayOrigin #filtersEditPanelOrigin="cdkOverlayOrigin"
@if (hasEditableFilters) {
<section class="tb-filters-edit flex flex-row items-center justify-start">
<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()"
matTooltip="{{ 'filter.filters' | translate }}"
[matTooltipPosition]="tooltipPosition">
<mat-icon>filter_list</mat-icon>
</button>
<span class="lt-lg:!hidden"
(click)="openEditMode()"
matTooltip="{{ 'filter.filters' | translate }}"
[matTooltipPosition]="tooltipPosition">
{{ 'filter.filters' | translate }}
</span>
</section>
{{ '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-70%': !isConstantKeyType
}"
formGroupName="key">
formGroupName="key">
<mat-form-field class="mat-block max-w-40% flex-full">
<mat-label translate>filter.key-type.key-type</mat-label>
<mat-select required formControlName="type">
<mat-option *ngFor="let type of entityKeyTypes" [value]="type">
{{entityKeyTypeTranslations.get(type) | translate}}
</mat-option>
@for (type of entityKeyTypes; track type) {
<mat-option [value]="type">
{{entityKeyTypeTranslations.get(type) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="mat-block max-w-60% flex-full">
@ -49,20 +51,26 @@
(focusin)="onFocus()"
[matAutocomplete]="keyName"
[matAutocompleteDisabled]="!showAutocomplete">
<button *ngIf="keyFilterFormGroup.get('key.key').value && showAutocomplete"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
@if (keyFilterFormGroup.get('key.key').value && showAutocomplete) {
<button
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
}
<mat-autocomplete autoActiveFirstOption #keyName="matAutocomplete">
<mat-option *ngFor="let keyName of filteredKeysName | async" [value]="keyName">
<span [innerHTML]="keyName | highlight:searchText"></span>
</mat-option>
@for (keyName of filteredKeysName | async; track keyName) {
<mat-option [value]="keyName">
<span [innerHTML]="keyName | highlight:searchText"></span>
</mat-option>
}
</mat-autocomplete>
<mat-error *ngIf="keyFilterFormGroup.get('key.key').hasError('required')">
{{ 'filter.key-name-required' | translate }}
</mat-error>
@if (keyFilterFormGroup.get('key.key').hasError('required')) {
<mat-error>
{{ 'filter.key-name-required' | translate }}
</mat-error>
}
</mat-form-field>
</section>
<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>
<span>{{ entityKeyValueTypes.get(keyFilterFormGroup.get('valueType').value)?.name | translate }}</span>
</mat-select-trigger>
<mat-option *ngFor="let valueType of entityKeyValueTypesKeys" [value]="valueType">
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
</mat-option>
@for (valueType of entityKeyValueTypesKeys; track valueType) {
<mat-option [value]="valueType">
<mat-icon class="tb-mat-18" svgIcon="{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).icon }}"></mat-icon>
<span>{{ entityKeyValueTypes.get(entityKeyValueTypeEnum[valueType]).name | translate }}</span>
</mat-option>
}
</mat-select>
<mat-error *ngIf="keyFilterFormGroup.get('valueType').hasError('required')">
{{ 'filter.value-type-required' | translate }}
</mat-error>
@if (keyFilterFormGroup.get('valueType').hasError('required')) {
<mat-error>
{{ 'filter.value-type-required' | translate }}
</mat-error>
}
</mat-form-field>
<div class="flex-1" *ngIf="isConstantKeyType">
<div class="flex flex-col" [ngSwitch]="keyFilterFormGroup.get('valueType').value">
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.STRING">
<mat-form-field class="mat-block">
<mat-label translate>filter.value</mat-label>
<input matInput required formControlName="value" placeholder="{{'filter.value' | translate}}">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.NUMERIC">
<mat-form-field class="mat-block">
<mat-label translate>filter.value</mat-label>
<input required type="number" matInput formControlName="value">
</mat-form-field>
</ng-template>
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.DATE_TIME">
<tb-datetime formControlName="value"
dateText="{{ 'filter.date' | translate }}"
required></tb-datetime>
</ng-template>
<ng-template [ngSwitchCase]="entityKeyValueTypeEnum.BOOLEAN">
<mat-checkbox formControlName="value" style="margin-top: 20px">
{{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
</ng-template>
@if (isConstantKeyType) {
<div class="flex-1">
<div class="flex flex-col">
@switch (keyFilterFormGroup.get('valueType').value) {
@case (entityKeyValueTypeEnum.STRING) {
<mat-form-field class="mat-block">
<mat-label translate>filter.value</mat-label>
<input matInput required formControlName="value" placeholder="{{'filter.value' | translate}}">
</mat-form-field>
}
@case (entityKeyValueTypeEnum.NUMERIC) {
<mat-form-field class="mat-block">
<mat-label translate>filter.value</mat-label>
<input required type="number" matInput formControlName="value">
</mat-form-field>
}
@case (entityKeyValueTypeEnum.DATE_TIME) {
<tb-datetime formControlName="value"
dateText="{{ 'filter.date' | translate }}"
required></tb-datetime>
}
@case (entityKeyValueTypeEnum.BOOLEAN) {
<mat-checkbox formControlName="value" style="margin-top: 20px">
{{ (keyFilterFormGroup.get('value').value ? 'value.true' : 'value.false') | translate }}
</mat-checkbox>
}
}
</div>
</div>
</div>
}
</section>
<tb-filter-predicate-list *ngIf="keyFilterFormGroup.get('valueType').value"
[allowUserDynamicSource]="data.allowUserDynamicSource"
[onlyUserDynamicSource]="isConstantKeyType"
[displayUserParameters]="data.displayUserParameters"
[valueType]="keyFilterFormGroup.get('valueType').value"
[key]="keyFilterFormGroup.get('key.key').value"
formControlName="predicates">
</tb-filter-predicate-list>
@if (keyFilterFormGroup.get('valueType').value) {
<tb-filter-predicate-list
[allowUserDynamicSource]="data.allowUserDynamicSource"
[onlyUserDynamicSource]="isConstantKeyType"
[displayUserParameters]="data.displayUserParameters"
[valueType]="keyFilterFormGroup.get('valueType').value"
[key]="keyFilterFormGroup.get('key.key').value"
formControlName="predicates">
</tb-filter-predicate-list>
}
</fieldset>
</div>
<div mat-dialog-actions class="flex items-center justify-end">
@ -130,11 +148,12 @@
cdkFocusInitial>
{{ (data.readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
type="submit"
*ngIf="!data.readonly"
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
@if (!data.readonly) {
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || keyFilterFormGroup.invalid || !keyFilterFormGroup.dirty">
{{ (data.isAdd ? 'action.add' : 'action.update') | translate }}
</button>
}
</div>
</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">
<mat-form-field hideRequiredMarker class="mat-block max-w-30% flex-full">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of numericOperations" [value]="operation">
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
</mat-option>
@for (operation of numericOperations; track operation) {
<mat-option [value]="operation">
{{numericOperationTranslations.get(numericOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
<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">
<mat-form-field hideRequiredMarker class="mat-block flex-1">
<mat-select required formControlName="operation" placeholder="{{'filter.operation.operation' | translate}}">
<mat-option *ngFor="let operation of stringOperations" [value]="operation">
{{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}}
</mat-option>
@for (operation of stringOperations; track operation) {
<mat-option [value]="operation">
{{stringOperationTranslations.get(stringOperationEnum[operation]) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
<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.
-->
<div *ngIf="show()" class="flex flex-row items-end justify-end" style="height: 100%; padding: 0 16px;">
<button class="lt-md:!hidden" mat-flat-button color="primary" (click)="sendNotification($event)">
<mat-icon>send</mat-icon>
<span translate>notification.send-notification</span>
</button>
<button class="mat-elevation-z0 gt-sm:!hidden" mat-mini-fab color="primary" (click)="sendNotification($event)">
<mat-icon>send</mat-icon>
</button>
</div>
@if (show()) {
<div class="flex flex-row items-end justify-end" style="height: 100%; padding: 0 16px;">
<button class="lt-md:!hidden" mat-flat-button color="primary" (click)="sendNotification($event)">
<mat-icon>send</mat-icon>
<span translate>notification.send-notification</span>
</button>
<button class="mat-elevation-z0 gt-sm:!hidden" mat-mini-fab color="primary" (click)="sendNotification($event)">
<mat-icon>send</mat-icon>
</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">
<div style="font-weight: 500; letter-spacing: .25px" translate>notification.notification</div>
<button mat-button color="primary"
*ngIf="(notifications$ | async)?.length"
(click)="markAsAllRead($event)">
{{ 'notification.mark-all-as-read' | translate }}
</button>
@if ((notifications$ | async)?.length) {
<button mat-button color="primary"
(click)="markAsAllRead($event)">
{{ 'notification.mark-all-as-read' | translate }}
</button>
}
</section>
<mat-divider></mat-divider>
<ng-container *ngIf="notifications$ | async; else loadingNotification">
<div *ngIf="(notifications$ | async).length; else emptyNotification" style="overflow: auto">
<section style="min-height: 100px; overflow: auto; padding: 6px 0;">
<div *ngFor="let notification of (notifications$ | async); let last = last; trackBy: trackById">
<tb-notification [notification]="notification"
[onClose]="onClose"
(markAsRead)="markAsRead($event)">
</tb-notification>
<mat-divider *ngIf="!last" style="margin: 4px"></mat-divider>
</div>
@if (notifications$ | async) {
@if ((notifications$ | async).length) {
<div style="overflow: auto">
<section style="min-height: 100px; overflow: auto; padding: 6px 0;">
@for (notification of (notifications$ | async); track notification.id.id; let last = $last) {
<div>
<tb-notification [notification]="notification"
[onClose]="onClose"
(markAsRead)="markAsRead($event)">
</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>
</div>
<mat-divider *ngIf="(notifications$ | async).length"></mat-divider>
<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>
}
} @else {
<div class="tb-no-data-available" style="margin: 20px; gap: 16px;">
<mat-spinner color="accent" diameter="65" strokeWidth="4"></mat-spinner>
<div class="tb-no-data-text" translate>notification.loading-notifications</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);
notifications$: Observable<Notification[]> = this.notificationSubscriber.notifications$.pipe(
filter(value => Array.isArray(value)),
notifications$: Observable<Notification[]> = (this.notificationSubscriber.notifications$ as Observable<Notification[] | null>).pipe(
filter((value): value is Notification[] => Array.isArray(value)),
shareReplay(1),
tap(() => setTimeout(() => this.cd.markForCheck()))
);
@ -91,8 +91,4 @@ export class ShowNotificationPopoverComponent extends PageComponent implements O
this.onClose();
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"
formControlName="defaultValue"
placeholder="{{ defaultValuePlaceholder | translate }}">
<mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('required')">
{{ defaultValueRequiredError | translate }}
</mat-error>
<mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('min')">
{{ defaultValueRangeError | translate }}
</mat-error>
<mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('max')">
{{ defaultValueRangeError | translate }}
</mat-error>
<mat-error *ngIf="alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('pattern')">
{{ defaultValuePatternError | translate }}
</mat-error>
@if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('required')) {
<mat-error>
{{ defaultValueRequiredError | translate }}
</mat-error>
}
@if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('min')) {
<mat-error>
{{ defaultValueRangeError | translate }}
</mat-error>
}
@if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('max')) {
<mat-error>
{{ defaultValueRangeError | translate }}
</mat-error>
}
@if (alarmDurationPredicateValueFormGroup.get('defaultValue').hasError('pattern')) {
<mat-error>
{{ defaultValuePatternError | translate }}
</mat-error>
}
</mat-form-field>
</div>
<div class="flex flex-1 flex-col" [class.!hidden]="!dynamicMode">
@ -44,9 +52,11 @@
<mat-option [value]="null">
{{'filter.no-dynamic-value' | translate}}
</mat-option>
<mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType">
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
@for (sourceType of dynamicValueSourceTypes; track sourceType) {
<mat-option [value]="sourceType">
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@ -55,13 +65,15 @@
<input matInput formControlName="sourceAttribute" placeholder="{{'filter.source-attribute' | translate}}">
</mat-form-field>
</div>
<div *ngIf="inheritMode"
class="flex flex-col"
style="padding-top: 6px">
<mat-checkbox formControlName="inherit">
{{ 'filter.inherit-owner' | translate}}
</mat-checkbox>
</div>
@if (inheritMode) {
<div
class="flex flex-col"
style="padding-top: 6px">
<mat-checkbox formControlName="inherit">
{{ 'filter.inherit-owner' | translate}}
</mat-checkbox>
</div>
}
</div>
</div>
<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">
{{'filter.no-dynamic-value' | translate}}
</mat-option>
<mat-option *ngFor="let sourceType of dynamicValueSourceTypes" [value]="sourceType">
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
@for (sourceType of dynamicValueSourceTypes; track sourceType) {
<mat-option [value]="sourceType">
{{dynamicValueSourceTypeTranslations.get(sourceType) | translate}}
</mat-option>
}
</mat-select>
</mat-form-field>
</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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
@ -41,35 +43,47 @@
<mat-form-field class="mat-block" hideRequiredMarker>
<mat-label translate>device-profile.condition-type</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let alarmConditionType of alarmConditionTypes" [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
@for (alarmConditionType of alarmConditionTypes; track alarmConditionType) {
<mat-option [value]="alarmConditionType">
{{ alarmConditionTypeTranslation.get(alarmConditionType) | translate }}
</mat-option>
}
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('spec.type').hasError('required')">
{{ 'device-profile.condition-type-required' | translate }}
</mat-error>
@if (conditionFormGroup.get('spec.type').hasError('required')) {
<mat-error>
{{ 'device-profile.condition-type-required' | translate }}
</mat-error>
}
</mat-form-field>
<div class="flex flex-row xs:flex-col gt-xs:gap-4" *ngIf="conditionFormGroup.get('spec.type').value != AlarmConditionType.SIMPLE">
<tb-alarm-duration-predicate-value
class="flex flex-1 flex-row"
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">
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-select formControlName="unit" required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="conditionFormGroup.get('spec.unit').hasError('required')">
{{ 'device-profile.condition-duration-time-unit-required' | translate }}
</mat-error>
</mat-form-field>
@if (conditionFormGroup.get('spec.type').value != AlarmConditionType.SIMPLE) {
<div class="flex flex-row xs:flex-col gt-xs:gap-4">
<tb-alarm-duration-predicate-value
class="flex flex-1 flex-row"
formControlName="predicate"
[alarmConditionType]="conditionFormGroup.get('spec.type').value"
>
</tb-alarm-duration-predicate-value>
@if (conditionFormGroup.get('spec.type').value == AlarmConditionType.DURATION) {
<div class="max-w-23% flex-full">
<mat-form-field class="mat-block" subscriptSizing="dynamic">
<mat-select formControlName="unit" required
placeholder="{{ 'device-profile.condition-duration-time-unit' | translate }}">
@for (timeUnit of timeUnits; track timeUnit) {
<mat-option [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
}
</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>
}
</section>
</div>
</fieldset>
@ -81,11 +95,12 @@
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
@if (!readonly) {
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || conditionFormGroup.invalid || !conditionFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
}
</div>
</form>

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

@ -23,8 +23,10 @@
required
addFilterPrompt="{{'device-profile.enter-alarm-rule-condition-prompt' | translate}}">
</tb-filter-text>
<span *ngIf="specText" class="tb-alarm-rule-condition-spec" [class.disabled]="disabled" [innerHTML]="specText">
</span>
@if (specText) {
<span class="tb-alarm-rule-condition-spec" [class.disabled]="disabled" [innerHTML]="specText">
</span>
}
</div>
<button mat-icon-button
[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-schedule-info formControlName="schedule">
</tb-alarm-schedule-info>
<div *ngIf="!disabled || alarmRuleFormGroup.get('alarmDetails').value" class="flex flex-row items-center justify-start">
<span class="tb-alarm-rule-details title" (click)="openEditDetailsDialog($event)">
{{ ('device-profile.alarm-rule-additional-info' | translate) + ': ' }}
</span>
<span *ngIf="alarmRuleFormGroup.get('alarmDetails').value" class="tb-alarm-rule-details"
(click)="openEditDetailsDialog($event)"
[innerHTML]="alarmDetailsText"></span>
<button mat-icon-button color="primary"
type="button"
(click)="openEditDetailsDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon>
</button>
</div>
<div *ngIf="!disabled || alarmRuleFormGroup.get('dashboardId').value" class="flex flex-col items-start justify-start gt-sm:flex-row gt-sm:items-center">
<span class="tb-alarm-rule-dashboard title">
{{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }}
</span>
<tb-dashboard-autocomplete class="tb-alarm-rule-dashboard dashboard"
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>
@if (!disabled || alarmRuleFormGroup.get('alarmDetails').value) {
<div class="flex flex-row items-center justify-start">
<span class="tb-alarm-rule-details title" (click)="openEditDetailsDialog($event)">
{{ ('device-profile.alarm-rule-additional-info' | translate) + ': ' }}
</span>
@if (alarmRuleFormGroup.get('alarmDetails').value) {
<span class="tb-alarm-rule-details"
(click)="openEditDetailsDialog($event)"
[innerHTML]="alarmDetailsText"></span>
}
<button mat-icon-button color="primary"
type="button"
(click)="openEditDetailsDialog($event)"
matTooltip="{{ (disabled ? 'action.view' : 'action.edit') | translate }}"
matTooltipPosition="above">
<mat-icon>{{ disabled ? 'visibility' : (alarmRuleFormGroup.get('alarmDetails').value ? 'edit' : 'add') }}</mat-icon>
</button>
</div>
}
@if (!disabled || alarmRuleFormGroup.get('dashboardId').value) {
<div class="flex flex-col items-start justify-start gt-sm:flex-row gt-sm:items-center">
<span class="tb-alarm-rule-dashboard title">
{{ ('device-profile.alarm-rule-mobile-dashboard' | translate) + ': ' }}
</span>
<tb-dashboard-autocomplete class="tb-alarm-rule-dashboard dashboard"
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>

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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content>
<tb-alarm-schedule
formControlName="alarmSchedule">
@ -39,11 +41,12 @@
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
@if (!readonly) {
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || alarmScheduleFormGroup.invalid || !alarmScheduleFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
}
</div>
</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">
<mat-form-field class="mat-block">
<mat-select formControlName="type" required placeholder="{{ 'device-profile.schedule-type' | translate }}">
<mat-option *ngFor="let alarmScheduleType of alarmScheduleTypes" [value]="alarmScheduleType">
{{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }}
</mat-option>
@for (alarmScheduleType of alarmScheduleTypes; track alarmScheduleType) {
<mat-option [value]="alarmScheduleType">
{{ alarmScheduleTypeTranslate.get(alarmScheduleType) | translate }}
</mat-option>
}
</mat-select>
<mat-error *ngIf="alarmScheduleForm.get('type').hasError('required')">
{{ 'device-profile.schedule-type-required' | translate }}
</mat-error>
@if (alarmScheduleForm.get('type').hasError('required')) {
<mat-error>
{{ 'device-profile.schedule-type-required' | translate }}
</mat-error>
}
</mat-form-field>
<div *ngIf="alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME">
<tb-timezone-select
userTimezoneByDefault
required
formControlName="timezone">
</tb-timezone-select>
<section *ngIf="alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME">
<tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_specific_schedule_format'></tb-alarm-dynamic-value>
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div class="flex flex-col gap-2 gt-md:flex-row">
<div class="flex flex-row gap-2">
<mat-checkbox *ngFor="let day of firstRowDays" [formControl]="weeklyRepeatControl(day)">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
</div>
<div class="flex flex-row gap-2">
<mat-checkbox *ngFor="let day of secondRowDays" [formControl]="weeklyRepeatControl(day)">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
</div>
</div>
@if (alarmScheduleForm.get('type').value !== alarmScheduleType.ANY_TIME) {
<div>
<tb-timezone-select
userTimezoneByDefault
required
formControlName="timezone">
</tb-timezone-select>
@if (alarmScheduleForm.get('type').value === alarmScheduleType.SPECIFIC_TIME) {
<section>
<tb-alarm-dynamic-value formControlName = 'dynamicValue' helpId = 'device-profile/alarm_specific_schedule_format'></tb-alarm-dynamic-value>
<div class="tb-small" style="margin-bottom: 0.5em" translate>device-profile.schedule-days</div>
<div class="flex flex-col gap-2 gt-md:flex-row">
<div class="flex flex-row gap-2">
@for (day of firstRowDays; track day) {
<mat-checkbox [formControl]="weeklyRepeatControl(day)">
{{ dayOfWeekTranslationsArray[day] | translate }}
</mat-checkbox>
}
</div>
<div class="flex flex-row gap-2">
@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')
? ('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="flex flex-row xs:flex-col gt-xs:gap-2">
<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-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="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-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="margin: auto">
<div style="text-align: center"
[innerHTML]="getSchedulerRangeText(alarmScheduleForm)">
</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 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 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-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="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-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="margin: auto">
<div style="text-align: center"
[innerHTML]="getSchedulerRangeText(alarmScheduleForm)">
</div>
</div>
</div>
<div class="flex flex-1 items-center justify-center"
style="text-align: center"
[innerHTML]="getSchedulerRangeText(itemsSchedulerForm.at(day))">
</div>
</div>
</div>
</section>
}
@if (alarmScheduleForm.get('type').value === alarmScheduleType.CUSTOM) {
<section>
<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>
@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')
? ('device-profile.schedule-days-of-week-required' | translate) : ''"></tb-error>
</section>
</div>
</section>
}
</div>
}
</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;
alarmScheduleTypes = Object.keys(AlarmScheduleType);
alarmScheduleTypes = Object.values(AlarmScheduleType);
alarmScheduleType = AlarmScheduleType;
alarmScheduleTypeTranslate = AlarmScheduleTypeTranslationMap;
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 *ngFor="let createAlarmRuleControl of createAlarmRulesFormArray().controls; let $index = index;
last as isLast;" class="flex flex-row items-center justify-start gap-2"
style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl">
<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-label translate>alarm.severity</mat-label>
<mat-select formControlName="severity"
required
placeholder="{{ 'device-profile.select-alarm-severity' | translate }}">
<mat-option *ngFor="let alarmSeverity of alarmSeverities" [value]="alarmSeverity"
[disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], $index)">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
</mat-select>
<mat-error *ngIf="createAlarmRuleControl.get('severity').hasError('required')">
{{ 'device-profile.alarm-severity-required' | translate }}
</mat-error>
</mat-form-field>
<mat-divider vertical></mat-divider>
<tb-alarm-rule formControlName="alarmRule" [deviceProfileId]="deviceProfileId" required class="flex-1">
</tb-alarm-rule>
@for (createAlarmRuleControl of createAlarmRulesFormArray().controls; track createAlarmRuleControl; let isLast = $last) {
<div class="flex flex-row items-center justify-start gap-2"
style="padding-bottom: 8px;" [formGroup]="createAlarmRuleControl">
<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-label translate>alarm.severity</mat-label>
<mat-select formControlName="severity"
required
placeholder="{{ 'device-profile.select-alarm-severity' | translate }}">
@for (alarmSeverity of alarmSeverities; track alarmSeverity) {
<mat-option [value]="alarmSeverity"
[disabled]="isDisabledSeverity(alarmSeverityEnum[alarmSeverity], $index)">
{{ alarmSeverityTranslationMap.get(alarmSeverityEnum[alarmSeverity]) | translate }}
</mat-option>
}
</mat-select>
@if (createAlarmRuleControl.get('severity').hasError('required')) {
<mat-error>
{{ 'device-profile.alarm-severity-required' | translate }}
</mat-error>
}
</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>
<button *ngIf="!disabled"
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 *ngIf="!createAlarmRulesFormArray().controls.length && !disabled">
}
@if (!createAlarmRulesFormArray().controls.length && !disabled) {
<div>
<span translate class="flex items-center justify-center" style="margin: 16px 0"
class="tb-prompt required">device-profile.add-create-alarm-rule-prompt</span>
</div>
<div *ngIf="!disabled">
<button mat-stroked-button color="primary"
type="button"
(click)="addCreateAlarmRule()"
matTooltip="{{ 'device-profile.add-create-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
{{ 'device-profile.add-create-alarm-rule' | translate }}
</button>
</div>
class="tb-prompt required">device-profile.add-create-alarm-rule-prompt</span>
</div>
}
@if (!disabled) {
<div>
<button mat-stroked-button color="primary"
type="button"
(click)="addCreateAlarmRule()"
matTooltip="{{ 'device-profile.add-create-alarm-rule' | translate }}"
matTooltipPosition="above">
<mat-icon class="button-icon">add_circle_outline</mat-icon>
{{ 'device-profile.add-create-alarm-rule' | translate }}
</button>
</div>
}
</div>

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

@ -24,13 +24,15 @@
</div>
</mat-panel-title>
<span class="flex-1"></span>
<button *ngIf="!disabled" mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeAlarm.emit()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
@if (!disabled) {
<button mat-icon-button style="min-width: 40px;"
type="button"
(click)="removeAlarm.emit()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
}
</div>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
@ -39,15 +41,21 @@
<mat-form-field class="flex-1" floatLabel="always">
<mat-label>{{'device-profile.alarm-type' | translate}}</mat-label>
<input required matInput formControlName="alarmType" placeholder="Enter alarm type">
<mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('required')">
{{ 'device-profile.alarm-type-required' | translate }}
</mat-error>
<mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('unique')">
{{ 'device-profile.alarm-type-unique' | translate }}
</mat-error>
<mat-error *ngIf="alarmFormGroup.get('alarmType').hasError('maxlength')">
{{ 'device-profile.alarm-type-max-length' | translate }}
</mat-error>
@if (alarmFormGroup.get('alarmType').hasError('required')) {
<mat-error>
{{ 'device-profile.alarm-type-required' | translate }}
</mat-error>
}
@if (alarmFormGroup.get('alarmType').hasError('unique')) {
<mat-error>
{{ 'device-profile.alarm-type-unique' | translate }}
</mat-error>
}
@if (alarmFormGroup.get('alarmType').hasError('maxlength')) {
<mat-error>
{{ 'device-profile.alarm-type-max-length' | translate }}
</mat-error>
}
</mat-form-field>
</div>
<mat-expansion-panel class="advanced-settings" [expanded]="false">
@ -62,25 +70,28 @@
<mat-checkbox formControlName="propagate" style="display: block;">
{{ 'device-profile.propagate-alarm' | translate }}
</mat-checkbox>
<section *ngIf="alarmFormGroup.get('propagate').value === true">
<mat-form-field floatLabel="always" class="mat-block">
<mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList [disabled]="disabled">
<mat-chip-row
*ngFor="let key of alarmFormGroup.get('propagateRelationTypes').value;"
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
<input matInput type="text" placeholder="{{'device-profile.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<mat-hint innerHTML="{{ 'device-profile.alarm-rule-relation-types-list-hint' | translate }}"></mat-hint>
</mat-form-field>
</section>
@if (alarmFormGroup.get('propagate').value === true) {
<section>
<mat-form-field floatLabel="always" class="mat-block">
<mat-label translate>device-profile.alarm-rule-relation-types-list</mat-label>
<mat-chip-grid #relationTypesChipList [disabled]="disabled">
@for (key of alarmFormGroup.get('propagateRelationTypes').value; track key) {
<mat-chip-row
(removed)="removeRelationType(key)">
{{key}}
<mat-icon matChipRemove>close</mat-icon>
</mat-chip-row>
}
<input matInput type="text" placeholder="{{'device-profile.alarm-rule-relation-types-list' | translate}}"
[matChipInputFor]="relationTypesChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matChipInputAddOnBlur
(matChipInputTokenEnd)="addRelationType($event)">
</mat-chip-grid>
<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;">
{{ 'device-profile.propagate-alarm-to-owner' | translate }}
</mat-checkbox>
@ -103,29 +114,35 @@
<tb-alarm-rule formControlName="clearRule" class="flex-1" [deviceProfileId]="deviceProfileId">
</tb-alarm-rule>
</div>
<button *ngIf="!disabled"
mat-icon-button color="primary" style="min-width: 40px;"
type="button"
(click)="removeClearAlarmRule()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>remove_circle_outline</mat-icon>
</button>
</div>
<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>
@if (!disabled) {
<button
mat-icon-button color="primary" style="min-width: 40px;"
type="button"
(click)="removeClearAlarmRule()"
matTooltip="{{ 'action.remove' | translate }}"
matTooltipPosition="above">
<mat-icon>remove_circle_outline</mat-icon>
</button>
}
</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>
</ng-template>
</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="tb-device-profile-alarms">
<div *ngFor="let alarmControl of alarmsFormArray.controls; trackBy: trackByAlarm;
let $index = index; last as isLast;"
class="flex flex-col" [class.pb-2]="!isLast">
<tb-device-profile-alarm [formControl]="alarmControl"
[expanded]="$index === 0"
[deviceProfileId]="deviceProfileId"
(removeAlarm)="removeAlarm($index)">
</tb-device-profile-alarm>
</div>
@for (alarmControl of alarmsFormArray.controls; track trackByAlarm($index, alarmControl); let isLast = $last) {
<div
class="flex flex-col" [class.pb-2]="!isLast">
<tb-device-profile-alarm [formControl]="alarmControl"
[expanded]="$index === 0"
[deviceProfileId]="deviceProfileId"
(removeAlarm)="removeAlarm($index)">
</tb-device-profile-alarm>
</div>
}
</div>
<div *ngIf="!alarmsFormArray.controls.length">
@if (!alarmsFormArray.controls.length) {
<div>
<span translate
class="tb-prompt flex items-center justify-center">device-profile.no-alarm-rules</span>
</div>
<div *ngIf="!disabled" style="padding-top: 16px;">
<button mat-raised-button color="primary"
type="button"
(click)="addAlarm()">
<span translate>device-profile.add-alarm-rule</span>
</button>
</div>
class="tb-prompt flex items-center justify-center">device-profile.no-alarm-rules</span>
</div>
}
@if (!disabled) {
<div style="padding-top: 16px;">
<button mat-raised-button color="primary"
type="button"
(click)="addAlarm()">
<span translate>device-profile.add-alarm-rule</span>
</button>
</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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content>
<fieldset [disabled]="isLoading$ | async">
<div class="flex flex-1 flex-col">
<mat-form-field class="mat-block">
<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-form-field>
</div>
@ -45,10 +47,12 @@
(click)="cancel()" cdkFocusInitial>
{{ 'action.cancel' | translate }}
</button>
<button *ngIf="!data.readonly" mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
@if (!data.readonly) {
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || editDetailsFormGroup.invalid || !editDetailsFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
}
</div>
</form>

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

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

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

@ -21,7 +21,9 @@
class="mat-block"
[appearance]="inlineField ? 'outline' : appearance"
[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"
#deviceProfileInput
formControlName="deviceProfile"
@ -31,56 +33,73 @@
(keypress)="deviceProfileEnter($event)"
[matAutocomplete]="deviceProfileAutocomplete"
[class.!hidden]="showDetailsPageLink && useDeviceProfileLink && disabled && selectDeviceProfileFormGroup.get('deviceProfile').value">
<a *ngIf="showDetailsPageLink && useDeviceProfileLink && selectDeviceProfileFormGroup.get('deviceProfile').value && disabled"
aria-label="Open device profile" [routerLink]=deviceProfileURL>
{{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }}
</a>
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value?.id && !disabled && editProfileEnabled"
type="button"
matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'device-profile.edit' | translate }}"
matTooltipPosition="above"
(click)="editDeviceProfile($event)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button mat-button color="primary" matSuffix
(click)="createDeviceProfile($event, '')"
*ngIf="!selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled && addNewProfile">
<span style="white-space: nowrap">{{ 'notification.create-new' | translate }}</span>
</button>
@if (showDetailsPageLink && useDeviceProfileLink && selectDeviceProfileFormGroup.get('deviceProfile').value && disabled) {
<a
aria-label="Open device profile" [routerLink]=deviceProfileURL>
{{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }}
</a>
}
@if (selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled) {
<button
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
}
@if (selectDeviceProfileFormGroup.get('deviceProfile').value?.id && !disabled && editProfileEnabled) {
<button
type="button"
matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'device-profile.edit' | translate }}"
matTooltipPosition="above"
(click)="editDeviceProfile($event)">
<mat-icon class="material-icons">edit</mat-icon>
</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
class="tb-autocomplete"
(closed)="onPanelClosed()"
#deviceProfileAutocomplete="matAutocomplete"
[displayWith]="displayDeviceProfileFn">
<mat-option *ngFor="let deviceProfile of filteredDeviceProfiles | async" [value]="deviceProfile">
<span [innerHTML]="deviceProfile.name | highlight:searchText"></span>
</mat-option>
<mat-option *ngIf="!(filteredDeviceProfiles | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText) || !addNewProfile; else searchNotEmpty">
<span translate>device-profile.no-device-profiles-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
@for (deviceProfile of filteredDeviceProfiles | async; track deviceProfile) {
<mat-option [value]="deviceProfile">
<span [innerHTML]="deviceProfile.name | highlight:searchText"></span>
</mat-option>
}
@if (!(filteredDeviceProfiles | async)?.length) {
<mat-option [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
@if (!textIsNotEmpty(searchText) || !addNewProfile) {
<div>
<span translate>device-profile.no-device-profiles-found</span>
</div>
} @else {
<span>
{{ translate.get('device-profile.no-device-profiles-matching',
{entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span>
<a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a>
</span>
</ng-template>
</div>
</mat-option>
{entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span>
<a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a>
</span>
}
</div>
</mat-option>
}
</mat-autocomplete>
<mat-error *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')">
{{ 'device-profile.device-profile-required' | translate }}
</mat-error>
<mat-hint *ngIf="hint && !disabled">{{ hint | translate }}</mat-hint>
@if (selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')) {
<mat-error>
{{ 'device-profile.device-profile-required' | translate }}
</mat-error>
}
@if (hint && !disabled) {
<mat-hint>{{ hint | translate }}</mat-hint>
}
</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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
} @else {
<div style="height: 4px;"></div>
}
<div mat-dialog-content>
<tb-device-profile
#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-label>{{ labelText | translate }}</mat-label>
<input matInput type="number" min="0" formControlName="time" required>
<mat-error *ngIf="timeUnitSelectFormGroup.get('time').hasError('required')">
{{ requiredText | translate }}
</mat-error>
<mat-error *ngIf="timeUnitSelectFormGroup.get('time').hasError('pattern')">
{{ patternText | translate }}
</mat-error>
<mat-error *ngIf="timeUnitSelectFormGroup.get('time').hasError('min')">
{{ (minText || patternText) | translate : {min: minTime/1000} }}
</mat-error>
@if (timeUnitSelectFormGroup.get('time').hasError('required')) {
<mat-error>
{{ requiredText | translate }}
</mat-error>
}
@if (timeUnitSelectFormGroup.get('time').hasError('pattern')) {
<mat-error>
{{ patternText | translate }}
</mat-error>
}
@if (timeUnitSelectFormGroup.get('time').hasError('min')) {
<mat-error>
{{ (minText || patternText) | translate : {min: minTime/1000} }}
</mat-error>
}
</mat-form-field>
<mat-form-field class="mat-block flex-1" [appearance]="appearance">
<mat-label translate>device-profile.condition-duration-time-unit</mat-label>
<mat-select formControlName="unit">
<mat-option *ngFor="let timeUnit of timeUnits" [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
@for (timeUnit of timeUnits; track timeUnit) {
<mat-option [value]="timeUnit">
{{ timeUnitTranslations.get(timeUnit) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
</section>

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

@ -16,11 +16,13 @@
-->
<div [formGroup]="deviceProfileConfigurationFormGroup">
<div [ngSwitch]="type">
<ng-template [ngSwitchCase]="deviceProfileType.DEFAULT">
<tb-default-device-profile-configuration
formControlName="configuration">
</tb-default-device-profile-configuration>
</ng-template>
<div>
@switch (type) {
@case (deviceProfileType.DEFAULT) {
<tb-default-device-profile-configuration
formControlName="configuration">
</tb-default-device-profile-configuration>
}
}
</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 [ngSwitch]="transportType">
<ng-template [ngSwitchCase]="deviceTransportType.DEFAULT">
<tb-default-device-profile-transport-configuration
formControlName="configuration">
</tb-default-device-profile-transport-configuration>
</ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.MQTT">
<tb-mqtt-device-profile-transport-configuration
formControlName="configuration">
</tb-mqtt-device-profile-transport-configuration>
</ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.COAP">
<tb-coap-device-profile-transport-configuration
formControlName="configuration">
</tb-coap-device-profile-transport-configuration>
</ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.LWM2M">
<tb-profile-lwm2m-device-transport-configuration
[isAdd]="isAdd"
formControlName="configuration">
</tb-profile-lwm2m-device-transport-configuration>
</ng-template>
<ng-template [ngSwitchCase]="deviceTransportType.SNMP">
<tb-snmp-device-profile-transport-configuration
formControlName="configuration">
</tb-snmp-device-profile-transport-configuration>
</ng-template>
<div>
@switch (transportType) {
@case (deviceTransportType.DEFAULT) {
<tb-default-device-profile-transport-configuration
formControlName="configuration">
</tb-default-device-profile-transport-configuration>
}
@case (deviceTransportType.MQTT) {
<tb-mqtt-device-profile-transport-configuration
formControlName="configuration">
</tb-mqtt-device-profile-transport-configuration>
}
@case (deviceTransportType.COAP) {
<tb-coap-device-profile-transport-configuration
formControlName="configuration">
</tb-coap-device-profile-transport-configuration>
}
@case (deviceTransportType.LWM2M) {
<tb-profile-lwm2m-device-transport-configuration
[isAdd]="isAdd"
formControlName="configuration">
</tb-profile-lwm2m-device-transport-configuration>
}
@case (deviceTransportType.SNMP) {
<tb-snmp-device-profile-transport-configuration
formControlName="configuration">
</tb-snmp-device-profile-transport-configuration>
}
}
</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>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
@if (isLoading$ | async) {
<mat-progress-bar color="warn" mode="indeterminate">
</mat-progress-bar>
}
<div mat-dialog-content>
<tb-lwm2m-attributes-key-list
[isResource]="isResource"
@ -42,11 +44,12 @@
(click)="cancel()" cdkFocusInitial>
{{ (readonly ? 'action.close' : 'action.cancel') | translate }}
</button>
<button mat-raised-button color="primary"
*ngIf="!readonly"
type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
@if (!readonly) {
<button mat-raised-button color="primary"
type="submit"
[disabled]="(isLoading$ | async) || attributeFormGroup.invalid || !attributeFormGroup.dirty">
{{ 'action.save' | translate }}
</button>
}
</div>
</form>

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

Loading…
Cancel
Save