Browse Source

Merge branch 'develop/3.5.2' of https://github.com/thingsboard/thingsboard into gateway_list

 Conflicts:
	ui-ngx/src/app/modules/common/modules-map.ts
pull/8337/head
Maksym Dudnik 3 years ago
parent
commit
82c546feb3
  1. 6
      application/src/main/data/json/system/widget_bundles/cards.json
  2. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java
  3. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java
  4. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java
  5. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java
  6. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java
  7. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java
  8. 12
      pom.xml
  9. 5
      ui-ngx/src/app/core/http/entity.service.ts
  10. 23
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  11. 3
      ui-ngx/src/app/modules/common/modules-map.ts
  12. 1
      ui-ngx/src/app/modules/home/components/dashboard-page/add-widget-dialog.component.html
  13. 18
      ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts
  14. 86
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html
  15. 154
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts
  16. 13
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.ts
  17. 170
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  18. 53
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  19. 426
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts
  20. 67
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  21. 76
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  22. 237
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts
  23. 2
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts
  24. 10
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts
  25. 144
      ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss
  26. 2
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.html
  27. 4
      ui-ngx/src/app/modules/home/components/widget/config/datasource.component.ts
  28. 6
      ui-ngx/src/app/modules/home/components/widget/config/datasources.component.ts
  29. 2
      ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts
  30. 58
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  31. 169
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss
  32. 2
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html
  33. 3
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts
  34. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts
  35. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html
  36. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts
  37. 25
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html
  38. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts
  39. 3
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html
  40. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts
  41. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts
  42. 11
      ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts
  43. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts
  44. 1
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  45. 116
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts
  46. 21
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  47. 3
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html
  48. 3
      ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html
  49. 67
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html
  50. 50
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss
  51. 21
      ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts
  52. 8
      ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html
  53. 28
      ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts
  54. 27
      ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html
  55. 26
      ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss
  56. 109
      ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts
  57. 7
      ui-ngx/src/app/shared/shared.module.ts
  58. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  59. 162
      ui-ngx/src/styles.scss

6
application/src/main/data/json/system/widget_bundles/cards.json

@ -104,7 +104,7 @@
"settingsDirective": "tb-simple-card-widget-settings", "settingsDirective": "tb-simple-card-widget-settings",
"hasBasicMode": true, "hasBasicMode": true,
"basicModeDirective": "tb-simple-card-basic-config", "basicModeDirective": "tb-simple-card-basic-config",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\"}" "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#ff5722\",\"color\":\"rgba(255, 255, 255, 0.87)\",\"padding\":\"16px\",\"settings\":{\"labelPosition\":\"top\"},\"title\":\"Simple card\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"°C\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true}"
} }
}, },
{ {
@ -143,7 +143,9 @@
"dataKeySettingsSchema": "", "dataKeySettingsSchema": "",
"settingsDirective": "tb-entities-table-widget-settings", "settingsDirective": "tb-entities-table-widget-settings",
"dataKeySettingsDirective": "tb-entities-table-key-settings", "dataKeySettingsDirective": "tb-entities-table-key-settings",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"entitiesTitle\":\"\",\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":true,\"entityNameColumnTitle\":\"\",\"displayEntityLabel\":false,\"displayEntityType\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"entityName\",\"useRowStyleFunction\":false},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#4caf50\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.8926244886945558,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#f44336\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"useCellContentFunction\":false,\"defaultColumnVisibility\":\"visible\",\"columnSelectionToDisplay\":\"enabled\"},\"_hash\":0.6401141393938932,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}]}" "hasBasicMode": true,
"basicModeDirective": "tb-entities-table-basic-config",
"defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}"
} }
}, },
{ {

2
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationId.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION; return EntityType.NOTIFICATION;

2
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRequestId.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION_REQUEST; return EntityType.NOTIFICATION_REQUEST;

2
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationRuleId.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION_RULE; return EntityType.NOTIFICATION_RULE;

2
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTargetId.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION_TARGET; return EntityType.NOTIFICATION_TARGET;

2
common/data/src/main/java/org/thingsboard/server/common/data/id/NotificationTemplateId.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId {
super(id); super(id);
} }
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.NOTIFICATION_TEMPLATE; return EntityType.NOTIFICATION_TEMPLATE;

2
common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.id;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.UUID; import java.util.UUID;
@ -34,6 +35,7 @@ public class QueueId extends UUIDBased implements EntityId {
return new QueueId(UUID.fromString(queueId)); return new QueueId(UUID.fromString(queueId));
} }
@ApiModelProperty(position = 2, required = true, value = "string", example = "QUEUE", allowableValues = "QUEUE")
@Override @Override
public EntityType getEntityType() { public EntityType getEntityType() {
return EntityType.QUEUE; return EntityType.QUEUE;

12
pom.xml

@ -111,7 +111,7 @@
<aws.sdk.version>1.11.747</aws.sdk.version> <aws.sdk.version>1.11.747</aws.sdk.version>
<pubsub.client.version>1.105.0</pubsub.client.version> <pubsub.client.version>1.105.0</pubsub.client.version>
<google.common.protos.version>2.1.0</google.common.protos.version> <!-- required by io.grpc:grpc-protobuf:1.38.0--> <google.common.protos.version>2.1.0</google.common.protos.version> <!-- required by io.grpc:grpc-protobuf:1.38.0-->
<azure-servicebus.version>3.2.0</azure-servicebus.version> <azure-servicebus.version>3.6.7</azure-servicebus.version>
<passay.version>1.5.0</passay.version> <passay.version>1.5.0</passay.version>
<ua-parser.version>1.5.4</ua-parser.version> <ua-parser.version>1.5.4</ua-parser.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version> <commons-beanutils.version>1.9.4</commons-beanutils.version>
@ -1836,6 +1836,16 @@
<groupId>com.microsoft.azure</groupId> <groupId>com.microsoft.azure</groupId>
<artifactId>azure-servicebus</artifactId> <artifactId>azure-servicebus</artifactId>
<version>${azure-servicebus.version}</version> <version>${azure-servicebus.version}</version>
<exclusions>
<exclusion>
<groupId>com.nimbusds</groupId>
<artifactId>content-type</artifactId>
</exclusion>
<exclusion>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.passay</groupId> <groupId>org.passay</groupId>

5
ui-ngx/src/app/core/http/entity.service.ts

@ -816,7 +816,8 @@ export class EntityService {
); );
} }
public getEntityKeysByEntityFilter(filter: EntityFilter, types: DataKeyType[], config?: RequestConfig): Observable<Array<DataKey>> { public getEntityKeysByEntityFilter(filter: EntityFilter, types: DataKeyType[],
entityTypes?: EntityType[], config?: RequestConfig): Observable<Array<DataKey>> {
if (!types.length) { if (!types.length) {
return of([]); return of([]);
} }
@ -832,7 +833,7 @@ export class EntityService {
entitiesKeysByQuery$ = of({ entitiesKeysByQuery$ = of({
attribute: [], attribute: [],
timeseries: [], timeseries: [],
entityTypes: [], entityTypes: entityTypes || [],
}); });
} }
return entitiesKeysByQuery$.pipe( return entitiesKeysByQuery$.pipe(

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

@ -366,25 +366,18 @@ export class DashboardUtilsService {
} }
const dataKeys = newDatasource.dataKeys; const dataKeys = newDatasource.dataKeys;
newDatasource.dataKeys = []; newDatasource.dataKeys = [];
dataKeys.forEach(dataKey => { if (widgetTypeDescriptor.type === widgetType.alarm) {
newDatasource.dataKeys.push(this.convertDataKeyFromWidgetType(widgetTypeDescriptor, config, dataKey)); dataKeys.forEach(dataKey => {
}); const newDataKey = deepClone(dataKey);
newDataKey.funcBody = null;
newDataKey.type = DataKeyType.alarm;
newDatasource.dataKeys.push(newDataKey);
});
}
} }
return newDatasource; return newDatasource;
} }
private convertDataKeyFromWidgetType(widgetTypeDescriptor: WidgetTypeDescriptor, config: WidgetConfig, dataKey: DataKey): DataKey {
const newDataKey = deepClone(dataKey);
newDataKey.funcBody = null;
if (widgetTypeDescriptor.type === widgetType.alarm) {
newDataKey.type = DataKeyType.alarm;
} else {
newDataKey.type = DataKeyType.timeseries;
newDataKey.name = newDataKey.label;
}
return newDataKey;
}
private validateAndUpdateState(state: DashboardState) { private validateAndUpdateState(state: DashboardState) {
if (!state.layouts) { if (!state.layouts) {
state.layouts = this.createDefaultLayouts(); state.layouts = this.createDefaultLayouts();

3
ui-ngx/src/app/modules/common/modules-map.ts

@ -307,7 +307,7 @@ import * as GatewayConnectorComponent from '@shared/components/device/gateway-co
import * as GatewayLogsComponent from '@shared/components/device/gateway-logs.component'; import * as GatewayLogsComponent from '@shared/components/device/gateway-logs.component';
import * as GatewayStatisticsComponent from '@shared/components/device/gateway-statistics.component'; import * as GatewayStatisticsComponent from '@shared/components/device/gateway-statistics.component';
import * as GatewayServiceRPCComponent from '@shared/components/device/gateway-service-rpc.component'; import * as GatewayServiceRPCComponent from '@shared/components/device/gateway-service-rpc.component';
import * as RuleChainSelectComponent from '@shared/components/rule-chain/rule-chain-select.component';
import { IModulesMap } from '@modules/common/modules-map.models'; import { IModulesMap } from '@modules/common/modules-map.models';
@ -433,6 +433,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/time/quick-time-interval.component': QuickTimeIntervalComponent, '@shared/components/time/quick-time-interval.component': QuickTimeIntervalComponent,
'@shared/components/dashboard-select.component': DashboardSelectComponent, '@shared/components/dashboard-select.component': DashboardSelectComponent,
'@shared/components/dashboard-select-panel.component': DashboardSelectPanelComponent, '@shared/components/dashboard-select-panel.component': DashboardSelectPanelComponent,
'@shared/components/rule-chain/rule-chain-select.component': RuleChainSelectComponent,
'@shared/components/time/datetime-period.component': DatetimePeriodComponent, '@shared/components/time/datetime-period.component': DatetimePeriodComponent,
'@shared/components/time/datetime.component': DatetimeComponent, '@shared/components/time/datetime.component': DatetimeComponent,
'@shared/components/time/timezone-select.component': TimezoneSelectComponent, '@shared/components/time/timezone-select.component': TimezoneSelectComponent,

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

@ -51,6 +51,7 @@
[widget]="widget" [widget]="widget"
[widgetConfigMode]="widgetConfigMode" [widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic" [hideHeader]="widgetConfigMode === widgetConfigModes.basic"
isAdd
formControlName="widgetConfig"> formControlName="widgetConfig">
</tb-widget-config> </tb-widget-config>
<tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill" <tb-widget-preview *ngIf="previewMode" class="tb-absolute-fill"

18
ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts

@ -25,11 +25,19 @@ import {
import { import {
WidgetActionsPanelComponent WidgetActionsPanelComponent
} from '@home/components/widget/config/basic/common/widget-actions-panel.component'; } from '@home/components/widget/config/basic/common/widget-actions-panel.component';
import {
EntitiesTableBasicConfigComponent
} from '@home/components/widget/config/basic/cards/entities-table-basic-config.component';
import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component';
import { DataKeyRowComponent } from '@home/components/widget/config/basic/common/data-key-row.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
WidgetActionsPanelComponent, WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -38,12 +46,16 @@ import {
], ],
exports: [ exports: [
WidgetActionsPanelComponent, WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
] ]
}) })
export class BasicWidgetConfigModule { export class BasicWidgetConfigModule {
} }
export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetConfigComponent>} = { export const basicWidgetConfigComponentsMap: {[key: string]: Type<IBasicWidgetConfigComponent>} = {
'tb-simple-card-basic-config': SimpleCardBasicConfigComponent 'tb-simple-card-basic-config': SimpleCardBasicConfigComponent,
'tb-entities-table-basic-config': EntitiesTableBasicConfigComponent
}; };

86
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.html

@ -0,0 +1,86 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<ng-container [formGroup]="entitiesTableWidgetConfigForm">
<tb-timewindow-config-panel *ngIf="displayTimewindowConfig"
[onlyHistoryTimewindow]="onlyHistoryTimewindow()"
formControlName="timewindowConfig">
</tb-timewindow-config-panel>
<tb-datasources
[configMode]="basicMode"
hideDataKeys
formControlName="datasources">
</tb-datasources>
<tb-data-keys-panel
panelTitle="{{ 'widgets.table.columns' | translate }}"
addKeyTitle="{{ 'widgets.table.add-column' | translate }}"
removeKeyTitle="{{ 'widgets.table.remove-column' | translate }}"
noKeysText="{{ 'widgets.table.no-columns' | translate }}"
hideDataKeyColor
[datasourceType]="datasource?.type"
[deviceId]="datasource?.deviceId"
[entityAliasId]="datasource?.entityAliasId"
formControlName="columns">
</tb-data-keys-panel>
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title" translate>widget-config.appearance</div>
<div class="tb-widget-config-row">
<mat-slide-toggle class="mat-slide" formControlName="showTitle">
{{ 'widget-config.card-title' | translate }}
</mat-slide-toggle>
<mat-form-field fxFlex appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="title" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-widget-config-row space-between same-padding">
<mat-slide-toggle class="mat-slide" formControlName="showTitleIcon">
{{ 'widget-config.card-icon' | translate }}
</mat-slide-toggle>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<tb-material-icon-select asBoxInput
[color]="entitiesTableWidgetConfigForm.get('iconColor').value"
formControlName="titleIcon">
</tb-material-icon-select>
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="iconColor">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.text-color' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
</div>
<div class="tb-widget-config-row space-between same-padding">
<div>{{ 'widget-config.background' | translate }}</div>
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="16px">
<mat-divider vertical></mat-divider>
<tb-color-input asBoxInput
formControlName="backgroundColor">
</tb-color-input>
</div>
</div>
</div>
<tb-widget-actions-panel
formControlName="actions">
</tb-widget-actions-panel>
</ng-container>

154
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/entities-table-basic-config.component.ts

@ -0,0 +1,154 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models';
import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import {
DataKey,
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@Component({
selector: 'tb-entities-table-basic-config',
templateUrl: './entities-table-basic-config.component.html',
styleUrls: ['../basic-config.scss']
})
export class EntitiesTableBasicConfigComponent extends BasicWidgetConfigComponent {
public get displayTimewindowConfig(): boolean {
const datasources = this.entitiesTableWidgetConfigForm.get('datasources').value;
return datasourcesHasAggregation(datasources);
}
public onlyHistoryTimewindow(): boolean {
const datasources = this.entitiesTableWidgetConfigForm.get('datasources').value;
return datasourcesHasOnlyComparisonAggregation(datasources);
}
public get datasource(): Datasource {
const datasources: Datasource[] = this.entitiesTableWidgetConfigForm.get('datasources').value;
if (datasources && datasources.length) {
return datasources[0];
} else {
return null;
}
}
entitiesTableWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.entitiesTableWidgetConfigForm;
}
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData, [{ name: 'name', type: DataKeyType.entityField }]);
}
protected onConfigSet(configData: WidgetConfigComponentData) {
this.entitiesTableWidgetConfigForm = this.fb.group({
timewindowConfig: [{
useDashboardTimewindow: configData.config.useDashboardTimewindow,
displayTimewindow: configData.config.useDashboardTimewindow,
timewindow: configData.config.timewindow
}, []],
datasources: [configData.config.datasources, []],
columns: [this.getColumns(configData.config.datasources), []],
showTitle: [configData.config.showTitle, []],
title: [configData.config.settings?.entitiesTitle, []],
showTitleIcon: [configData.config.showTitleIcon, []],
titleIcon: [configData.config.titleIcon, []],
iconColor: [configData.config.iconColor, []],
color: [configData.config.color, []],
backgroundColor: [configData.config.backgroundColor, []],
actions: [configData.config.actions || {}, []]
});
}
protected prepareOutputConfig(config: any): WidgetConfigComponentData {
this.widgetConfig.config.useDashboardTimewindow = config.timewindowConfig.useDashboardTimewindow;
this.widgetConfig.config.displayTimewindow = config.timewindowConfig.displayTimewindow;
this.widgetConfig.config.timewindow = config.timewindowConfig.timewindow;
this.widgetConfig.config.datasources = config.datasources;
this.setColumns(config.columns, this.widgetConfig.config.datasources);
this.widgetConfig.config.actions = config.actions;
this.widgetConfig.config.showTitle = config.showTitle;
this.widgetConfig.config.settings = this.widgetConfig.config.settings || {};
this.widgetConfig.config.settings.entitiesTitle = config.title;
this.widgetConfig.config.showTitleIcon = config.showTitleIcon;
this.widgetConfig.config.titleIcon = config.titleIcon;
this.widgetConfig.config.iconColor = config.iconColor;
this.widgetConfig.config.color = config.color;
this.widgetConfig.config.backgroundColor = config.backgroundColor;
return this.widgetConfig;
}
protected validatorTriggers(): string[] {
return ['showTitle', 'showTitleIcon'];
}
protected updateValidators(emitEvent: boolean, trigger?: string) {
const showTitle: boolean = this.entitiesTableWidgetConfigForm.get('showTitle').value;
const showTitleIcon: boolean = this.entitiesTableWidgetConfigForm.get('showTitleIcon').value;
if (showTitle) {
this.entitiesTableWidgetConfigForm.get('title').enable();
this.entitiesTableWidgetConfigForm.get('showTitleIcon').enable({emitEvent: false});
if (showTitleIcon) {
this.entitiesTableWidgetConfigForm.get('titleIcon').enable();
this.entitiesTableWidgetConfigForm.get('iconColor').enable();
} else {
this.entitiesTableWidgetConfigForm.get('titleIcon').disable();
this.entitiesTableWidgetConfigForm.get('iconColor').disable();
}
} else {
this.entitiesTableWidgetConfigForm.get('title').disable();
this.entitiesTableWidgetConfigForm.get('showTitleIcon').disable({emitEvent: false});
this.entitiesTableWidgetConfigForm.get('titleIcon').disable();
this.entitiesTableWidgetConfigForm.get('iconColor').disable();
}
this.entitiesTableWidgetConfigForm.get('title').updateValueAndValidity({emitEvent});
this.entitiesTableWidgetConfigForm.get('showTitleIcon').updateValueAndValidity({emitEvent: false});
this.entitiesTableWidgetConfigForm.get('titleIcon').updateValueAndValidity({emitEvent});
this.entitiesTableWidgetConfigForm.get('iconColor').updateValueAndValidity({emitEvent});
}
private getColumns(datasources?: Datasource[]): DataKey[] {
if (datasources && datasources.length) {
return datasources[0].dataKeys || [];
}
return [];
}
private setColumns(columns: DataKey[], datasources?: Datasource[]) {
if (datasources && datasources.length) {
datasources[0].dataKeys = columns;
}
}
}

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

@ -23,13 +23,15 @@ import { WidgetConfigComponentData } from '@home/models/widget-component.models'
import { import {
Datasource, Datasource,
datasourcesHasAggregation, datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation datasourcesHasOnlyComparisonAggregation,
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
@Component({ @Component({
selector: 'tb-simple-card-basic-config', selector: 'tb-simple-card-basic-config',
templateUrl: './simple-card-basic-config.component.html', templateUrl: './simple-card-basic-config.component.html',
styleUrls: ['../basic-config.scss', '../../widget-config.scss'] styleUrls: ['../basic-config.scss']
}) })
export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent { export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
@ -46,14 +48,19 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
simpleCardWidgetConfigForm: UntypedFormGroup; simpleCardWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) { private fb: UntypedFormBuilder) {
super(store); super(store, widgetConfigComponent);
} }
protected configForm(): UntypedFormGroup { protected configForm(): UntypedFormGroup {
return this.simpleCardWidgetConfigForm; return this.simpleCardWidgetConfigForm;
} }
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData, [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }]);
}
protected onConfigSet(configData: WidgetConfigComponentData) { protected onConfigSet(configData: WidgetConfigComponentData) {
this.simpleCardWidgetConfigForm = this.fb.group({ this.simpleCardWidgetConfigForm = this.fb.group({
timewindowConfig: [{ timewindowConfig: [{

170
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -0,0 +1,170 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div [formGroup]="keyRowFormGroup" class="tb-data-key-row">
<mat-form-field fxFlex class="tb-inline-field tb-key-field" subscriptSizing="dynamic">
<mat-chip-grid #chipList>
<mat-chip-row class="tb-datakey-chip" *ngIf="modelValue.type"
(removed)="removeKey()">
<div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="4px" class="tb-attribute-chip">
<div class="tb-chip-labels">
<div class="tb-chip-label">
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon>
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.attribute"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon>
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.entityField"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon>
<mat-icon class="tb-mat-18 tb-datakey-icon" *ngIf="modelValue.type === dataKeyTypes.timeseries"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">timeline</mat-icon>
</ng-container>
</div>
<div class="tb-chip-label">
<strong>
<ng-container *ngTemplateOutlet="keyName"></ng-container>
</strong>
</div>
</div>
<button type="button"
(click)="editKey()" mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">edit</mat-icon>
</button>
<button matChipRemove
type="button"
mat-icon-button class="tb-mat-24">
<mat-icon class="tb-mat-18">close</mat-icon>
</button>
</div>
</mat-chip-row>
<input matInput
type="text"
placeholder="{{ 'widget-config.set' | translate }}"
#keyInput
[formControl]="keyFormControl"
matAutocompleteOrigin
[fxHide]="!!modelValue.type"
[readonly]="!!modelValue.type"
#origin="matAutocompleteOrigin"
[matAutocompleteConnectedTo]="origin"
(focusin)="onKeyInputFocus()"
(drop)="$event.preventDefault();"
[matAutocomplete]="keyAutocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addKey($event)"
/>
</mat-chip-grid>
<mat-autocomplete #keyAutocomplete="matAutocomplete"
class="tb-autocomplete"
panelWidth="fit-content"
[displayWith]="displayKeyFn">
<mat-option *ngFor="let key of filteredKeys | async" [value]="key">
<span style="white-space: nowrap;">
<ng-container *ngIf="isEntityDatasource">
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above">notifications</mat-icon>
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.attribute"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"></mat-icon>
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.entityField"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"></mat-icon>
<mat-icon class="tb-datakey-icon" *ngIf="key.type === dataKeyTypes.timeseries"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above">timeline</mat-icon>
</ng-container>
<span [innerHTML]="key.name | highlight:keySearchText"></span>
</span>
</mat-option>
<mat-option *ngIf="!(filteredKeys | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(keySearchText); else searchNotEmpty">
<span translate>entity.no-keys-found</span>
</div>
<ng-template #searchNotEmpty>
<span>
{{ translate.get('entity.no-key-matching',
{key: truncate.transform(keySearchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span *ngIf="!isEntityDatasource; else createEntityKey">
<a translate (click)="createKey(keySearchText)">entity.create-new-key</a>
</span>
<ng-template #createEntityKey>
<span>{{'entity.create-new-key' | translate }} </span>
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.alarm"
matTooltip="{{'datakey.alarm' | translate }}"
matTooltipPosition="above"
(click)="createKey(keySearchText, dataKeyTypes.alarm)">notifications</mat-icon>
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.latest || widgetType === widgetTypes.alarm"
matTooltip="{{'datakey.attributes' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-a-circle-outline"
(click)="createKey(keySearchText, dataKeyTypes.attribute)"></mat-icon>
<mat-icon class="tb-datakey-icon new-key" *ngIf="widgetType === widgetTypes.latest || widgetType === widgetTypes.alarm"
matTooltip="{{'datakey.entity-field' | translate }}"
matTooltipPosition="above" svgIcon="mdi:alpha-e-circle-outline"
(click)="createKey(keySearchText, dataKeyTypes.entityField)"></mat-icon>
<mat-icon class="tb-datakey-icon new-key"
matTooltip="{{'datakey.timeseries' | translate }}"
matTooltipPosition="above"
(click)="createKey(keySearchText, dataKeyTypes.timeseries)">timeline</mat-icon>
</ng-template>
</ng-template>
</div>
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field fxFlex class="tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="label" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
<div *ngIf="!hideDataKeyColor" class="tb-color-field">
<tb-color-input asBoxInput
formControlName="color">
</tb-color-input>
</div>
<div class="tb-units-field">
<tb-widget-units *ngIf="displayUnitsOrDigits"
formControlName="units">
</tb-widget-units>
</div>
<div class="tb-decimals-field">
<mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field center number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
</div>
<ng-template #keyName>
<ng-container *ngIf="dataKeyHasPostprocessing(); else keyName">
<span>f(</span><ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container><span>)</span>
</ng-container>
<ng-template #keyName>
<ng-container *ngTemplateOutlet="keyNameTemplate"></ng-container>
</ng-template>
</ng-template>
<ng-template #keyNameTemplate>
<ng-container *ngIf="dataKeyHasAggregation(); else keyName;">
<span class="tb-agg-func">{{ modelValue?.aggregationType }}</span><span>({{ modelValue?.name }})</span>
</ng-container>
<ng-template #keyName>
<span>{{modelValue?.name}}</span>
</ng-template>
</ng-template>

53
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-data-key-row {
height: 38px;
display: flex;
flex-direction: row;
gap: 12px;
padding-left: 12px;
.mat-mdc-form-field.tb-inline-field.tb-key-field {
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) {
.mat-mdc-form-field-infix {
padding-top: 0;
padding-bottom: 6px;
.mdc-evolution-chip-set .mdc-evolution-chip {
margin: 0;
}
input.mat-mdc-chip-input {
height: 32px;
margin-left: 0;
}
}
}
.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip {
.tb-attribute-chip {
.tb-chip-labels {
background: transparent;
}
}
}
}
.tb-color-field, .tb-units-field, .tb-decimals-field {
width: 60px;
display: flex;
flex-direction: row;
place-content: center;
align-items: center;
}
}

426
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.ts

@ -0,0 +1,426 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
OnChanges,
OnInit,
SimpleChanges,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
ValidationErrors
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKey, DatasourceType, JsonSettingsSchema, Widget, widgetType } from '@shared/models/widget.models';
import { DataKeysPanelComponent } from '@home/components/widget/config/basic/common/data-keys-panel.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { AggregationType } from '@shared/models/time/time.models';
import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes';
import { MatChipGrid, MatChipInputEvent } from '@angular/material/chips';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { MatAutocomplete, MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { Observable, of } from 'rxjs';
import { filter, map, mergeMap, publishReplay, refCount, share, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { TruncatePipe } from '@shared/pipe/truncate.pipe';
import {
DataKeyConfigDialogComponent,
DataKeyConfigDialogData
} from '@home/components/widget/config/data-key-config-dialog.component';
import { deepClone } from '@core/utils';
import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models';
export const dataKeyRowValidator = (control: AbstractControl): ValidationErrors | null => {
const dataKey: DataKey = control.value;
if (!dataKey || !dataKey.type || !dataKey.name) {
return {
dataKey: true
};
}
return null;
};
@Component({
selector: 'tb-data-key-row',
templateUrl: './data-key-row.component.html',
styleUrls: ['./data-key-row.component.scss', '../../data-keys.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DataKeyRowComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class DataKeyRowComponent implements ControlValueAccessor, OnInit, OnChanges {
dataKeyTypes = DataKeyType;
widgetTypes = widgetType;
separatorKeysCodes: number[] = [ENTER, COMMA, SEMICOLON];
@ViewChild('keyInput') keyInput: ElementRef<HTMLInputElement>;
@ViewChild('keyAutocomplete') matAutocomplete: MatAutocomplete;
@ViewChild(MatAutocompleteTrigger) autocomplete: MatAutocompleteTrigger;
@ViewChild('chipList') chipList: MatChipGrid;
@Input()
disabled: boolean;
@Input()
datasourceType: DatasourceType;
@Input()
entityAliasId: string;
@Input()
deviceId: string;
keyFormControl: UntypedFormControl;
keyRowFormGroup: UntypedFormGroup;
modelValue: DataKey;
filteredKeys: Observable<Array<DataKey>>;
keySearchText = '';
private latestKeySearchTextResult: Array<DataKey> = null;
private keyFetchObservable$: Observable<Array<DataKey>> = null;
get dataKeyType(): DataKeyType {
return this.dataKeysPanelComponent.dataKeyType;
}
get alarmKeys(): Array<DataKey> {
return this.dataKeysPanelComponent.alarmKeys;
}
get functionTypeKeys(): Array<DataKey> {
return this.dataKeysPanelComponent.functionTypeKeys;
}
get hideDataKeyColor(): boolean {
return this.dataKeysPanelComponent.hideDataKeyColor;
}
get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
get callbacks(): DataKeysCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
get widget(): Widget {
return this.widgetConfigComponent.widget;
}
get dashboard(): Dashboard {
return this.widgetConfigComponent.dashboard;
}
get aliasController(): IAliasController {
return this.widgetConfigComponent.aliasController;
}
get datakeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
}
get dataKeySettingsDirective(): string {
return this.widgetConfigComponent.modelValue?.dataKeySettingsDirective;
}
get isEntityDatasource(): boolean {
return [DatasourceType.device, DatasourceType.entity].includes(this.datasourceType);
}
get displayUnitsOrDigits() {
return this.modelValue.type && ![ DataKeyType.alarm, DataKeyType.entityField, DataKeyType.count ].includes(this.modelValue.type);
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private cd: ChangeDetectorRef,
public translate: TranslateService,
public truncate: TruncatePipe,
private dataKeysPanelComponent: DataKeysPanelComponent,
private widgetConfigComponent: WidgetConfigComponent) {
}
ngOnInit() {
this.keyFormControl = this.fb.control('');
this.keyRowFormGroup = this.fb.group({
label: [null, []],
color: [null, []],
units: [null, []],
decimals: [null, []],
});
this.keyRowFormGroup.valueChanges.subscribe(
() => this.updateModel()
);
this.filteredKeys = this.keyFormControl.valueChanges
.pipe(
tap((value: string | DataKey) => {
if (value && typeof value !== 'string') {
this.addKeyFromChipValue(value);
} else if (value === null) {
this.clearKeyChip(this.keyInput.nativeElement.value);
}
}),
filter((value) => typeof value === 'string'),
map((value) => value ? (typeof value === 'string' ? value : value.name) : ''),
mergeMap(name => this.fetchKeys(name) ),
share()
);
}
private reset() {
if (this.keyInput) {
this.keyInput.nativeElement.value = '';
}
this.keyFormControl.patchValue('', {emitEvent: false});
this.latestKeySearchTextResult = null;
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['deviceId', 'entityAliasId'].includes(propName)) {
this.clearKeySearchCache();
} else if (['datasourceType'].includes(propName)) {
if ([DatasourceType.device, DatasourceType.entity].includes(change.previousValue) &&
[DatasourceType.device, DatasourceType.entity].includes(change.currentValue)) {
this.clearKeySearchCache();
} else {
this.clearKeySearchCache();
setTimeout(() => {
this.reset();
}, 1);
}
}
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.keyRowFormGroup.disable({emitEvent: false});
} else {
this.keyRowFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DataKey): void {
this.modelValue = value || {} as DataKey;
this.keyRowFormGroup.patchValue(
{
label: value?.label,
color: value?.color,
units: value?.units,
decimals: value?.decimals
}, {emitEvent: false}
);
this.cd.markForCheck();
}
dataKeyHasAggregation(): boolean {
return this.widgetConfigComponent.widgetType === widgetType.latest && this.modelValue?.type === DataKeyType.timeseries
&& this.modelValue?.aggregationType && this.modelValue?.aggregationType !== AggregationType.NONE;
}
dataKeyHasPostprocessing(): boolean {
return !!this.modelValue?.postFuncBody;
}
displayKeyFn(key?: DataKey): string | undefined {
return key ? key.name : undefined;
}
createKey(name: string, dataKeyType: DataKeyType = this.dataKeyType) {
this.addKeyFromChipValue({name: name ? name.trim() : '', type: dataKeyType});
}
addKey(event: MatChipInputEvent): void {
const value = event.value;
if ((value || '').trim() && this.dataKeyType) {
this.addKeyFromChipValue({name: value.trim(), type: this.dataKeyType});
} else {
this.clearKeyChip();
}
}
editKey() {
this.dialog.open<DataKeyConfigDialogComponent, DataKeyConfigDialogData, DataKey>(DataKeyConfigDialogComponent,
{
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
dataKey: deepClone(this.modelValue),
dataKeySettingsSchema: this.datakeySettingsSchema,
dataKeySettingsDirective: this.dataKeySettingsDirective,
dashboard: this.dashboard,
aliasController: this.aliasController,
widget: this.widget,
widgetType: this.widgetType,
deviceId: this.deviceId,
entityAliasId: this.entityAliasId,
showPostProcessing: this.widgetType !== widgetType.alarm,
callbacks: this.callbacks,
hideDataKeyLabel: false,
hideDataKeyColor: this.hideDataKeyColor,
hideDataKeyUnits: !this.displayUnitsOrDigits,
hideDataKeyDecimals: !this.displayUnitsOrDigits
}
}).afterClosed().subscribe((updatedDataKey) => {
if (updatedDataKey) {
this.modelValue = updatedDataKey;
this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false});
this.keyRowFormGroup.get('color').patchValue(this.modelValue.color, {emitEvent: false});
this.keyRowFormGroup.get('units').patchValue(this.modelValue.units, {emitEvent: false});
this.keyRowFormGroup.get('decimals').patchValue(this.modelValue.decimals, {emitEvent: false});
this.updateModel();
}
});
}
removeKey() {
this.modelValue = {} as DataKey;
this.updateModel();
this.clearKeyChip();
}
textIsNotEmpty(text: string): boolean {
return text && text.length > 0;
}
clearKeyChip(value: string = '', focus = true) {
this.autocomplete.closePanel();
this.keyInput.nativeElement.value = value;
this.keyFormControl.patchValue(value, {emitEvent: focus});
if (focus) {
setTimeout(() => {
this.keyInput.nativeElement.blur();
this.keyInput.nativeElement.focus();
}, 0);
}
}
onKeyInputFocus() {
if (!this.modelValue.type) {
this.keyFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
}
}
private fetchKeys(searchText?: string): Observable<Array<DataKey>> {
if (this.keySearchText !== searchText || this.latestKeySearchTextResult === null) {
this.keySearchText = searchText;
const dataKeyFilter = this.createDataKeyFilter(this.keySearchText);
return this.getKeys().pipe(
map(name => name.filter(dataKeyFilter)),
tap(res => this.latestKeySearchTextResult = res)
);
}
return of(this.latestKeySearchTextResult);
}
private getKeys(): Observable<Array<DataKey>> {
if (this.keyFetchObservable$ === null) {
let fetchObservable: Observable<Array<DataKey>>;
if (this.datasourceType === DatasourceType.function) {
const targetKeysList = this.widgetType === widgetType.alarm ? this.alarmKeys : this.functionTypeKeys;
fetchObservable = of(targetKeysList);
} else if (this.datasourceType === DatasourceType.entity && this.entityAliasId ||
this.datasourceType === DatasourceType.device && this.deviceId) {
const dataKeyTypes = [DataKeyType.timeseries];
if (this.widgetType === widgetType.latest || this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.attribute);
dataKeyTypes.push(DataKeyType.entityField);
if (this.widgetType === widgetType.alarm) {
dataKeyTypes.push(DataKeyType.alarm);
}
}
if (this.datasourceType === DatasourceType.device) {
fetchObservable = this.callbacks.fetchEntityKeysForDevice(this.deviceId, dataKeyTypes);
} else {
fetchObservable = this.callbacks.fetchEntityKeys(this.entityAliasId, dataKeyTypes);
}
} else {
fetchObservable = of([]);
}
this.keyFetchObservable$ = fetchObservable.pipe(
publishReplay(1),
refCount()
);
}
return this.keyFetchObservable$;
}
private createDataKeyFilter(query: string): (key: DataKey) => boolean {
const lowercaseQuery = query.toLowerCase();
return key => key.name.toLowerCase().startsWith(lowercaseQuery);
}
private addKeyFromChipValue(chip: DataKey) {
this.modelValue = this.callbacks.generateDataKey(chip.name, chip.type, this.datakeySettingsSchema);
if (!this.keyRowFormGroup.get('label').value) {
this.keyRowFormGroup.get('label').patchValue(this.modelValue.label, {emitEvent: false});
}
this.updateModel();
this.clearKeyChip('', false);
}
private clearKeySearchCache() {
this.keySearchText = '';
this.keyFetchObservable$ = null;
this.latestKeySearchTextResult = null;
}
private updateModel() {
const value: DataKey = this.keyRowFormGroup.value;
this.modelValue = {...this.modelValue, ...value};
this.propagateChange(this.modelValue);
}
}

67
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -0,0 +1,67 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div class="tb-widget-config-panel">
<div class="tb-widget-config-panel-title">{{ panelTitle }}</div>
<div class="tb-data-keys-table">
<div class="tb-data-keys-header">
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.key</div>
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.label</div>
<div *ngIf="!hideDataKeyColor" class="tb-data-keys-header-cell tb-color-header" translate>datakey.color</div>
<div class="tb-data-keys-header-cell tb-units-header" translate>widget-config.units-short</div>
<div class="tb-data-keys-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
<div class="tb-data-keys-header-cell tb-actions-header"></div>
</div>
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-data-keys-body tb-drop-list" cdkDropList cdkDropListOrientation="vertical"
(cdkDropListDropped)="keyDrop($event)">
<div cdkDrag class="tb-data-keys-table-row tb-draggable" *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey;
let $index = index;">
<tb-data-key-row fxFlex
[formControl]="keyControl"
[datasourceType]="datasourceType"
[deviceId]="deviceId"
[entityAliasId]="entityAliasId">
</tb-data-key-row>
<div class="tb-data-keys-table-row-buttons">
<button type="button"
mat-icon-button
(click)="removeKey($index)"
[matTooltip]="removeKeyTitle"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button
type="button"
cdkDragHandle
matTooltip="{{ 'action.drag' | translate }}"
matTooltipPosition="above">
<mat-icon>drag_indicator</mat-icon>
</button>
</div>
</div>
</div>
</div>
<div>
<button type="button" mat-stroked-button color="primary" (click)="addKey()">
{{ addKeyTitle }}
</button>
</div>
</div>
<ng-template #noKeys>
<span fxLayoutAlign="center center"
class="tb-prompt">{{ noKeysText }}</span>
</ng-template>

76
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-data-keys-table {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 12px;
.tb-data-keys-header {
height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 12px;
padding-left: 12px;
.tb-data-keys-header-cell {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
&.tb-color-header, &.tb-units-header, &.tb-decimals-header {
width: 60px;
}
&.tb-actions-header {
width: 76px;
}
}
}
.tb-data-keys-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.tb-prompt {
height: 38px;
}
}
.tb-data-keys-table-row {
height: 38px;
display: flex;
flex-direction: row;
gap: 12px;
background: #fff;
.tb-data-keys-table-row-buttons {
display: flex;
flex-direction: row;
button.mat-mdc-icon-button.mat-mdc-button-base {
padding: 7px;
width: 38px;
height: 38px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
}
}
}

237
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.ts

@ -0,0 +1,237 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import {
ChangeDetectorRef,
Component,
forwardRef,
Input,
OnChanges,
OnInit,
SimpleChanges,
ViewEncapsulation
} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormControl,
UntypedFormGroup,
Validator
} from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKey, DatasourceType, JsonSettingsSchema, widgetType } from '@shared/models/widget.models';
import { dataKeyRowValidator } from '@home/components/widget/config/basic/common/data-key-row.component';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { alarmFields } from '@shared/models/alarm.models';
import { UtilsService } from '@core/services/utils.service';
import { DataKeysCallbacks } from '@home/components/widget/config/data-keys.component.models';
import { coerceBoolean } from '@shared/decorators/coercion';
@Component({
selector: 'tb-data-keys-panel',
templateUrl: './data-keys-panel.component.html',
styleUrls: ['./data-keys-panel.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DataKeysPanelComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DataKeysPanelComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class DataKeysPanelComponent implements ControlValueAccessor, OnInit, OnChanges, Validator {
@Input()
disabled: boolean;
@Input()
panelTitle: string;
@Input()
addKeyTitle: string;
@Input()
removeKeyTitle: string;
@Input()
noKeysText: string;
@Input()
datasourceType: DatasourceType;
@Input()
entityAliasId: string;
@Input()
deviceId: string;
@Input()
@coerceBoolean()
hideDataKeyColor = false;
dataKeyType: DataKeyType;
alarmKeys: Array<DataKey>;
functionTypeKeys: Array<DataKey>;
keysListFormGroup: UntypedFormGroup;
get widgetType(): widgetType {
return this.widgetConfigComponent.widgetType;
}
get callbacks(): DataKeysCallbacks {
return this.widgetConfigComponent.widgetConfigCallbacks;
}
get datakeySettingsSchema(): JsonSettingsSchema {
return this.widgetConfigComponent.modelValue?.dataKeySettingsSchema;
}
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private dialog: MatDialog,
private cd: ChangeDetectorRef,
private utils: UtilsService,
private widgetConfigComponent: WidgetConfigComponent) {
}
ngOnInit() {
this.keysListFormGroup = this.fb.group({
keys: [this.fb.array([]), []]
});
this.keysListFormGroup.valueChanges.subscribe(
(val) => this.propagateChange(this.keysListFormGroup.get('keys').value)
);
this.alarmKeys = [];
for (const name of Object.keys(alarmFields)) {
this.alarmKeys.push({
name,
type: DataKeyType.alarm
});
}
this.functionTypeKeys = [];
for (const type of this.utils.getPredefinedFunctionsList()) {
this.functionTypeKeys.push({
name: type,
type: DataKeyType.function
});
}
this.updateParams();
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (['datasourceType'].includes(propName)) {
this.updateParams();
}
}
}
}
private updateParams() {
if (this.datasourceType === DatasourceType.function) {
this.dataKeyType = DataKeyType.function;
} else {
if (this.widgetType !== widgetType.latest && this.widgetType !== widgetType.alarm) {
this.dataKeyType = DataKeyType.timeseries;
} else {
this.dataKeyType = null;
}
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (isDisabled) {
this.keysListFormGroup.disable({emitEvent: false});
} else {
this.keysListFormGroup.enable({emitEvent: false});
}
}
writeValue(value: DataKey[] | undefined): void {
this.keysListFormGroup.setControl('keys', this.prepareKeysFormArray(value), {emitEvent: false});
}
public validate(c: UntypedFormControl) {
return this.keysListFormGroup.valid ? null : {
dataKeyRows: {
valid: false,
},
};
}
keyDrop(event: CdkDragDrop<string[]>) {
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray;
const key = keysArray.at(event.previousIndex);
keysArray.removeAt(event.previousIndex);
keysArray.insert(event.currentIndex, key);
}
keysFormArray(): UntypedFormArray {
return this.keysListFormGroup.get('keys') as UntypedFormArray;
}
trackByKey(index: number, keyControl: AbstractControl): any {
return keyControl;
}
removeKey(index: number) {
(this.keysListFormGroup.get('keys') as UntypedFormArray).removeAt(index);
}
addKey() {
const dataKey = this.callbacks.generateDataKey('', null, this.datakeySettingsSchema);
dataKey.label = '';
dataKey.decimals = 0;
const keysArray = this.keysListFormGroup.get('keys') as UntypedFormArray;
const keyControl = this.fb.control(dataKey, [dataKeyRowValidator]);
keysArray.push(keyControl);
}
private prepareKeysFormArray(keys: DataKey[] | undefined): UntypedFormArray {
const keysControls: Array<AbstractControl> = [];
if (keys) {
keys.forEach((key) => {
keysControls.push(this.fb.control(key, [dataKeyRowValidator]));
});
}
return this.fb.array(keysControls);
}
}

2
ui-ngx/src/app/modules/home/components/widget/config/basic/common/widget-actions-panel.component.ts

@ -29,7 +29,7 @@ import { MatDialog } from '@angular/material/dialog';
@Component({ @Component({
selector: 'tb-widget-actions-panel', selector: 'tb-widget-actions-panel',
templateUrl: './widget-actions-panel.component.html', templateUrl: './widget-actions-panel.component.html',
styleUrls: ['../../widget-config.scss'], styleUrls: [],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

10
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.ts

@ -155,6 +155,7 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
private dataKeySettingsData: JsonFormComponentData; private dataKeySettingsData: JsonFormComponentData;
private alarmKeys: Array<DataKey>; private alarmKeys: Array<DataKey>;
private functionTypeKeys: Array<DataKey>;
filteredKeys: Observable<Array<string>>; filteredKeys: Observable<Array<string>>;
private latestKeySearchResult: Array<string> = null; private latestKeySearchResult: Array<string> = null;
@ -183,6 +184,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
type: DataKeyType.alarm type: DataKeyType.alarm
}); });
} }
this.functionTypeKeys = [];
for (const type of this.utils.getPredefinedFunctionsList()) {
this.functionTypeKeys.push({
name: type,
type: DataKeyType.function
});
}
if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema || if (this.dataKeySettingsSchema && this.dataKeySettingsSchema.schema ||
this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) { this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) {
this.displayAdvanced = true; this.displayAdvanced = true;
@ -396,6 +404,8 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
let fetchObservable: Observable<Array<DataKey>>; let fetchObservable: Observable<Array<DataKey>>;
if (this.modelValue.type === DataKeyType.alarm) { if (this.modelValue.type === DataKeyType.alarm) {
fetchObservable = of(this.alarmKeys); fetchObservable = of(this.alarmKeys);
} else if (this.modelValue.type === DataKeyType.function) {
fetchObservable = of(this.functionTypeKeys);
} else { } else {
if (this.deviceId || this.entityAliasId) { if (this.deviceId || this.entityAliasId) {
const dataKeyTypes = [this.modelValue.type]; const dataKeyTypes = [this.modelValue.type];

144
ui-ngx/src/app/modules/home/components/widget/config/data-keys.component.scss

@ -22,92 +22,92 @@
input.tb-dragging { input.tb-dragging {
display: none; display: none;
} }
}
.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip { .mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip {
overflow: hidden; overflow: hidden;
line-height: 20px; line-height: 20px;
height: 32px; height: 32px;
&.mdc-evolution-chip--with-trailing-action { &.mdc-evolution-chip--with-trailing-action {
.mdc-evolution-chip__action--primary { .mdc-evolution-chip__action--primary {
padding-left: 4px; padding-left: 4px;
padding-right: 12px; padding-right: 12px;
}
} }
}
.mat-mdc-chip-action { .mat-mdc-chip-action {
overflow: hidden;
.mat-mdc-chip-action-label {
overflow: hidden; overflow: hidden;
.mat-mdc-chip-action-label {
overflow: hidden;
}
} }
.tb-attribute-chip { }
max-width: 100%; .tb-attribute-chip {
color: rgb(66, 66, 66); max-width: 100%;
.tb-chip-drag-handle { color: rgb(66, 66, 66);
padding: 3px; .tb-chip-drag-handle {
height: 24px; padding: 3px;
cursor: move; height: 24px;
mat-icon { cursor: move;
pointer-events: none; mat-icon {
} pointer-events: none;
} }
.tb-chip-labels { }
display: flex; .tb-chip-labels {
flex-direction: row; display: flex;
align-items: center; flex-direction: row;
min-width: 0; align-items: center;
background: rgba(0, 0, 0, 0.04); min-width: 0;
border-radius: 100px; background: rgba(0, 0, 0, 0.04);
padding: 2px 10px; border-radius: 100px;
.tb-chip-label { padding: 2px 10px;
font-weight: normal; .tb-chip-label {
font-size: 14px; font-weight: normal;
line-height: 20px; font-size: 14px;
overflow: hidden; line-height: 20px;
text-overflow: ellipsis; overflow: hidden;
white-space: nowrap; text-overflow: ellipsis;
.mat-icon.tb-datakey-icon { white-space: nowrap;
margin-right: 4px; .mat-icon.tb-datakey-icon {
margin-left: 4px; margin-right: 4px;
} margin-left: 4px;
.tb-agg-func {
font-style: italic;
color: #0c959c;
}
} }
.tb-chip-separator { .tb-agg-func {
white-space: pre; font-style: italic;
color: #0c959c;
} }
} }
.mat-mdc-chip-remove.mat-mdc-icon-button { .tb-chip-separator {
color: inherit; white-space: pre;
opacity: inherit;
} }
} }
.mat-mdc-chip-remove.mat-mdc-icon-button {
&.tb-datakey-chip-dnd-placeholder { color: inherit;
min-width: 120px; opacity: inherit;
border: 2px dashed rgba(0, 0, 0, 0.2);
}
&.tb-chip-dragging {
display: none;
} }
}
&.tb-datakey-chip-dnd-placeholder {
min-width: 120px;
border: 2px dashed rgba(0, 0, 0, 0.2);
}
&.tb-chip-dragging {
display: none;
}
.tb-dragging-chip-image-fill {
background-color: rgba(0,0,0,0.3);
border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px);
display: none;
pointer-events: none;
}
.tb-dragging-chip-image {
background-color: var(--mdc-chip-elevated-container-color, transparent);
border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px);
overflow: hidden;
height: 32px;
line-height: 20px;
.tb-dragging-chip-image-fill { .tb-dragging-chip-image-fill {
background-color: rgba(0,0,0,0.3); display: block;
border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px);
display: none;
pointer-events: none;
}
.tb-dragging-chip-image {
background-color: var(--mdc-chip-elevated-container-color, transparent);
border-radius: var(--mdc-chip-container-shape-radius, 16px 16px 16px 16px);
overflow: hidden;
height: 32px;
line-height: 20px;
.tb-dragging-chip-image-fill {
display: block;
}
} }
} }
} }

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

@ -61,7 +61,7 @@
</mat-form-field> </mat-form-field>
</ng-template> </ng-template>
</section> </section>
<section fxLayout="column" fxLayoutAlign="stretch" fxFlex> <section *ngIf="!hideDataKeys" fxLayout="column" fxLayoutAlign="stretch" fxFlex>
<tb-data-keys class="tb-data-keys" fxFlex <tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType" [widgetType]="widgetType"
[datasourceType]="datasourceFormGroup.get('type').value" [datasourceType]="datasourceFormGroup.get('type').value"

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

@ -139,6 +139,10 @@ export class DatasourceComponent implements ControlValueAccessor, OnInit, Valida
return this.datasourcesComponent?.hideDataKeyDecimals; return this.datasourcesComponent?.hideDataKeyDecimals;
} }
public get hideDataKeys(): boolean {
return this.datasourcesComponent?.hideDataKeys;
}
@Input() @Input()
disabled: boolean; disabled: boolean;

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

@ -46,7 +46,7 @@ import { coerceBoolean } from '@shared/decorators/coercion';
@Component({ @Component({
selector: 'tb-datasources', selector: 'tb-datasources',
templateUrl: './datasources.component.html', templateUrl: './datasources.component.html',
styleUrls: ['./datasources.component.scss', './widget-config.scss'], styleUrls: ['./datasources.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -104,6 +104,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@coerceBoolean() @coerceBoolean()
hideDataKeyDecimals = false; hideDataKeyDecimals = false;
@Input()
@coerceBoolean()
hideDataKeys = false;
@Input() @Input()
configMode: WidgetConfigMode; configMode: WidgetConfigMode;

2
ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts

@ -31,7 +31,7 @@ export interface TimewindowConfigData {
@Component({ @Component({
selector: 'tb-timewindow-config-panel', selector: 'tb-timewindow-config-panel',
templateUrl: './timewindow-config-panel.component.html', templateUrl: './timewindow-config-panel.component.html',
styleUrls: ['./widget-config.scss'], styleUrls: [],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,

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

@ -23,12 +23,15 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { WidgetConfigMode } from '@shared/models/widget.models'; import { DataKey, DatasourceType, KeyInfo, WidgetConfigMode } from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { DataKeyType } from '@shared/models/telemetry/telemetry.models';
import { isDefinedAndNotNull } from '@core/utils';
export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks;
export interface IBasicWidgetConfigComponent { export interface IBasicWidgetConfigComponent {
isAdd: boolean;
widgetConfig: WidgetConfigComponentData; widgetConfig: WidgetConfigComponentData;
widgetConfigChanged: Observable<WidgetConfigComponentData>; widgetConfigChanged: Observable<WidgetConfigComponentData>;
validateConfig(): boolean; validateConfig(): boolean;
@ -40,6 +43,8 @@ export interface IBasicWidgetConfigComponent {
export abstract class BasicWidgetConfigComponent extends PageComponent implements export abstract class BasicWidgetConfigComponent extends PageComponent implements
IBasicWidgetConfigComponent, OnInit, AfterViewInit { IBasicWidgetConfigComponent, OnInit, AfterViewInit {
isAdd = false;
basicMode = WidgetConfigMode.basic; basicMode = WidgetConfigMode.basic;
widgetConfigValue: WidgetConfigComponentData; widgetConfigValue: WidgetConfigComponentData;
@ -56,7 +61,8 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
widgetConfigChangedEmitter = new EventEmitter<WidgetConfigComponentData>(); widgetConfigChangedEmitter = new EventEmitter<WidgetConfigComponentData>();
widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable(); widgetConfigChanged = this.widgetConfigChangedEmitter.asObservable();
protected constructor(@Inject(Store) protected store: Store<AppState>) { protected constructor(@Inject(Store) protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent) {
super(store); super(store);
} }
@ -65,12 +71,15 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
ngAfterViewInit(): void { ngAfterViewInit(): void {
setTimeout(() => { setTimeout(() => {
if (!this.validateConfig()) { if (!this.validateConfig()) {
this.onConfigChanged(this.prepareOutputConfig(this.configForm().value)); this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue()));
} }
}, 0); }, 0);
} }
protected setupConfig(widgetConfig: WidgetConfigComponentData) { protected setupConfig(widgetConfig: WidgetConfigComponentData) {
if (this.isAdd) {
this.setupDefaults(widgetConfig);
}
this.onConfigSet(widgetConfig); this.onConfigSet(widgetConfig);
this.updateValidators(false); this.updateValidators(false);
for (const trigger of this.validatorTriggers()) { for (const trigger of this.validatorTriggers()) {
@ -83,11 +92,13 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
this.updateValidators(true, trigger); this.updateValidators(true, trigger);
}); });
} }
this.configForm().valueChanges.subscribe((updated: any) => { this.configForm().valueChanges.subscribe(() => {
this.onConfigChanged(this.prepareOutputConfig(updated)); this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue()));
}); });
} }
protected setupDefaults(configData: WidgetConfigComponentData) {}
protected updateValidators(emitEvent: boolean, trigger?: string) { protected updateValidators(emitEvent: boolean, trigger?: string) {
} }
@ -108,6 +119,41 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
return this.configForm().valid; return this.configForm().valid;
} }
protected setupDefaultDatasource(configData: WidgetConfigComponentData, keys?: DataKey[]) {
let datasources = configData.config.datasources;
if (!datasources || !datasources.length) {
datasources = [
{
type: DatasourceType.device,
dataKeys: []
}
];
configData.config.datasources = datasources;
}
let dataKeys = datasources[0].dataKeys;
if (!dataKeys) {
dataKeys = [];
datasources[0].dataKeys = dataKeys;
}
if (keys && keys.length) {
dataKeys.length = 0;
keys.forEach(key => {
const dataKey =
this.widgetConfigComponent.widgetConfigCallbacks.generateDataKey(key.name, key.type, configData.dataKeySettingsSchema);
if (key.label) {
dataKey.label = key.label;
}
if (key.units) {
dataKey.units = key.units;
}
if (isDefinedAndNotNull(key.decimals)) {
dataKey.decimals = key.decimals;
}
dataKeys.push(dataKey);
});
}
}
protected abstract configForm(): UntypedFormGroup; protected abstract configForm(): UntypedFormGroup;
protected abstract onConfigSet(configData: WidgetConfigComponentData); protected abstract onConfigSet(configData: WidgetConfigComponentData);

169
ui-ngx/src/app/modules/home/components/widget/config/widget-config.scss

@ -1,169 +0,0 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-widget-config-panel {
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04);
border-radius: 4px;
padding: 16px;
gap: 16px;
display: flex;
flex-direction: column;
color: rgba(0, 0, 0, 0.87);
letter-spacing: 0.15px;
position: relative;
&.no-padding-bottom {
padding-bottom: 0;
}
&.stroked {
box-shadow: none;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
}
}
.tb-widget-config-panel-title {
font-weight: 500;
font-size: 16px;
}
.tb-widget-config-panel-hint {
font-size: 12px;
color: #808080;
}
.tb-widget-config-row {
height: 56px;
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
padding-left: 16px;
padding-right: 12px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
&.same-padding {
padding-right: 16px;
}
&.space-between {
justify-content: space-between;
}
.mat-divider-vertical {
height: 56px;
}
}
:host ::ng-deep {
.mat-slide {
margin: 8px 0;
.mdc-form-field>label {
font-weight: 400;
font-size: 16px;
line-height: 24px;
margin-left: 12px;
}
}
.slide-block {
display: block;
&:not(:last-child) {
margin-bottom: 8px;
}
}
.mat-mdc-form-field {
&.center {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
.mat-mdc-form-field-infix {
.mdc-text-field__input {
text-align: center;
}
}
}
}
&.number {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
padding-right: 4px;
.mat-mdc-form-field-infix {
width: 80px;
input.mdc-text-field__input[type=number]::-webkit-inner-spin-button,
input.mdc-text-field__input[type=number]::-webkit-outer-spin-button {
opacity: 1;
}
}
}
}
}
.tb-widget-config-row {
.mat-mdc-form-field {
.mat-mdc-text-field-wrapper.mdc-text-field--outlined {
padding-right: 12px;
padding-left: 12px;
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
border-color: rgba(0, 0, 0, 0.12);
}
}
.mat-mdc-form-field-infix {
padding-top: 7px;
padding-bottom: 7px;
min-height: 38px;
width: 72px;
}
}
}
}
.tb-widget-config-panel {
.mat-expansion-panel {
&.tb-settings {
box-shadow: none;
.mat-content {
overflow: visible;
}
.mat-expansion-panel-header {
font-weight: 500;
font-size: 16px;
line-height: 24px;
letter-spacing: 0.25px;
padding: 0;
.mat-content {
flex: 0;
white-space: nowrap;
}
&:hover {
background: none;
}
.mat-expansion-indicator {
height: 32px;
padding: 2px;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0;
}
}
.tb-json-object-panel, .tb-css-content-panel {
margin: 0 0 8px;
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
}
}

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

@ -15,6 +15,6 @@
limitations under the License. limitations under the License.
--> -->
<mat-form-field appearance="outline" class="center" subscriptSizing="dynamic"> <mat-form-field appearance="outline" class="tb-inline-field center" subscriptSizing="dynamic">
<input matInput [formControl]="unitsFormControl" placeholder="{{ 'widget-config.set' | translate }}"> <input matInput [formControl]="unitsFormControl" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>

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

@ -20,7 +20,7 @@ import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilde
@Component({ @Component({
selector: 'tb-widget-units', selector: 'tb-widget-units',
templateUrl: './widget-units.component.html', templateUrl: './widget-units.component.html',
styleUrls: ['./widget-config.scss'], styleUrls: [],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -43,6 +43,7 @@ export class WidgetUnitsComponent implements ControlValueAccessor, OnInit {
ngOnInit() { ngOnInit() {
this.unitsFormControl = this.fb.control('', []); this.unitsFormControl = this.fb.control('', []);
this.unitsFormControl.valueChanges.subscribe(val => this.propagateChange(val));
} }
writeValue(units?: string): void { writeValue(units?: string): void {

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/simple-card-widget-settings.component.ts

@ -23,7 +23,7 @@ import { AppState } from '@core/core.state';
@Component({ @Component({
selector: 'tb-simple-card-widget-settings', selector: 'tb-simple-card-widget-settings',
templateUrl: './simple-card-widget-settings.component.html', templateUrl: './simple-card-widget-settings.component.html',
styleUrls: ['../../../config/widget-config.scss'] styleUrls: []
}) })
export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent { export class SimpleCardWidgetSettingsComponent extends WidgetSettingsComponent {

25
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.html

@ -66,4 +66,29 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-column-visibility</mat-label>
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section> </section>

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-key-settings.component.ts

@ -43,7 +43,9 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent
useCellStyleFunction: false, useCellStyleFunction: false,
cellStyleFunction: '', cellStyleFunction: '',
useCellContentFunction: false, useCellContentFunction: false,
cellContentFunction: '' cellContentFunction: '',
defaultColumnVisibility: 'visible',
columnSelectionToDisplay: 'enabled'
}; };
} }
@ -53,6 +55,8 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent
cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], cellStyleFunction: [settings.cellStyleFunction, [Validators.required]],
useCellContentFunction: [settings.useCellContentFunction, []], useCellContentFunction: [settings.useCellContentFunction, []],
cellContentFunction: [settings.cellContentFunction, [Validators.required]], cellContentFunction: [settings.cellContentFunction, [Validators.required]],
defaultColumnVisibility: [settings.defaultColumnVisibility, []],
columnSelectionToDisplay: [settings.columnSelectionToDisplay, []],
}); });
} }

25
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.html

@ -73,4 +73,29 @@
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.default-column-visibility</mat-label>
<mat-select formControlName="defaultColumnVisibility">
<mat-option [value]="'visible'">
{{ 'widgets.table.column-visibility-visible' | translate }}
</mat-option>
<mat-option [value]="'hidden'">
{{ 'widgets.table.column-visibility-hidden' | translate }}
</mat-option>
<mat-option [value]="'hidden-mobile'">
{{ 'widgets.table.column-visibility-hidden-mobile' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-selection-to-display</mat-label>
<mat-select formControlName="columnSelectionToDisplay">
<mat-option [value]="'enabled'">
{{ 'widgets.table.column-selection-to-display-enabled' | translate }}
</mat-option>
<mat-option [value]="'disabled'">
{{ 'widgets.table.column-selection-to-display-disabled' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section> </section>

6
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-latest-key-settings.component.ts

@ -44,7 +44,9 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom
useCellStyleFunction: false, useCellStyleFunction: false,
cellStyleFunction: '', cellStyleFunction: '',
useCellContentFunction: false, useCellContentFunction: false,
cellContentFunction: '' cellContentFunction: '',
defaultColumnVisibility: 'visible',
columnSelectionToDisplay: 'enabled'
}; };
} }
@ -56,6 +58,8 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom
cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], cellStyleFunction: [settings.cellStyleFunction, [Validators.required]],
useCellContentFunction: [settings.useCellContentFunction, []], useCellContentFunction: [settings.useCellContentFunction, []],
cellContentFunction: [settings.cellContentFunction, [Validators.required]], cellContentFunction: [settings.cellContentFunction, [Validators.required]],
defaultColumnVisibility: [settings.defaultColumnVisibility, []],
columnSelectionToDisplay: [settings.columnSelectionToDisplay, []],
}); });
} }

3
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.html

@ -23,6 +23,9 @@
<mat-checkbox formControlName="enableSearch"> <mat-checkbox formControlName="enableSearch">
{{ 'widgets.table.enable-search' | translate }} {{ 'widgets.table.enable-search' | translate }}
</mat-checkbox> </mat-checkbox>
<mat-checkbox formControlName="enableSelectColumnDisplay">
{{ 'widgets.table.enable-select-column-display' | translate }}
</mat-checkbox>
</section> </section>
<section fxLayout="column" fxFlex> <section fxLayout="column" fxFlex>
<mat-checkbox formControlName="enableStickyHeader"> <mat-checkbox formControlName="enableStickyHeader">

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/timeseries-table-widget-settings.component.ts

@ -41,6 +41,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
protected defaultSettings(): WidgetSettings { protected defaultSettings(): WidgetSettings {
return { return {
enableSearch: true, enableSearch: true,
enableSelectColumnDisplay: true,
enableStickyHeader: true, enableStickyHeader: true,
enableStickyAction: true, enableStickyAction: true,
reserveSpaceForHiddenAction: 'true', reserveSpaceForHiddenAction: 'true',
@ -59,6 +60,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
protected onSettingsSet(settings: WidgetSettings) { protected onSettingsSet(settings: WidgetSettings) {
this.timeseriesTableWidgetSettingsForm = this.fb.group({ this.timeseriesTableWidgetSettingsForm = this.fb.group({
enableSearch: [settings.enableSearch, []], enableSearch: [settings.enableSearch, []],
enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []],
enableStickyHeader: [settings.enableStickyHeader, []], enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []], enableStickyAction: [settings.enableStickyAction, []],
reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []], reserveSpaceForHiddenAction: [settings.reserveSpaceForHiddenAction, []],

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.ts

@ -215,7 +215,7 @@ export class ValueSourceComponent extends PageComponent implements OnInit, Contr
mergeMap((aliasInfo) => { mergeMap((aliasInfo) => {
return this.entityService.getEntityKeysByEntityFilter( return this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter, aliasInfo.entityFilter,
dataKeyTypes, dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true} {ignoreLoading: true, ignoreErrors: true}
).pipe( ).pipe(
catchError(() => of([])) catchError(() => of([]))

11
ui-ngx/src/app/modules/home/components/widget/lib/settings/control/device-key-autocomplete.component.ts

@ -15,7 +15,13 @@
/// ///
import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
UntypedFormBuilder,
UntypedFormGroup,
Validators
} from '@angular/forms';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
@ -27,6 +33,7 @@ import { catchError, map, mergeMap, publishReplay, refCount, tap } from 'rxjs/op
import { DataKey } from '@shared/models/widget.models'; import { DataKey } from '@shared/models/widget.models';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { EntityType } from '@shared/models/entity-type.models';
@Component({ @Component({
selector: 'tb-device-key-autocomplete', selector: 'tb-device-key-autocomplete',
@ -199,7 +206,7 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI
mergeMap((aliasInfo) => { mergeMap((aliasInfo) => {
return this.entityService.getEntityKeysByEntityFilter( return this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter, aliasInfo.entityFilter,
dataKeyTypes, dataKeyTypes, [EntityType.DEVICE],
{ignoreLoading: true, ignoreErrors: true} {ignoreLoading: true, ignoreErrors: true}
).pipe( ).pipe(
catchError(() => of([])) catchError(() => of([]))

2
ui-ngx/src/app/modules/home/components/widget/lib/settings/map/image-map-provider-settings.component.ts

@ -236,7 +236,7 @@ export class ImageMapProviderSettingsComponent extends PageComponent implements
mergeMap((aliasInfo) => { mergeMap((aliasInfo) => {
return this.entityService.getEntityKeysByEntityFilter( return this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter, aliasInfo.entityFilter,
dataKeyTypes, dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true} {ignoreLoading: true, ignoreErrors: true}
).pipe( ).pipe(
catchError(() => of([])) catchError(() => of([]))

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

@ -31,6 +31,7 @@ type ColumnSelectionOptions = 'enabled' | 'disabled';
export interface TableWidgetSettings { export interface TableWidgetSettings {
enableSearch: boolean; enableSearch: boolean;
enableSelectColumnDisplay: boolean;
enableStickyAction: boolean; enableStickyAction: boolean;
enableStickyHeader: boolean; enableStickyHeader: boolean;
displayPagination: boolean; displayPagination: boolean;

116
ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts

@ -19,9 +19,12 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
Injector,
Input, Input,
OnDestroy,
OnInit, OnInit,
QueryList, QueryList,
StaticProvider,
ViewChild, ViewChild,
ViewChildren, ViewChildren,
ViewContainerRef ViewContainerRef
@ -64,8 +67,9 @@ import {
CellStyleInfo, CellStyleInfo,
checkHasActions, checkHasActions,
constructTableCssString, constructTableCssString,
DisplayColumn,
getCellContentInfo, getCellContentInfo,
getCellStyleInfo, getCellStyleInfo, getColumnDefaultVisibility, getColumnSelectionAvailability,
getRowStyleInfo, getRowStyleInfo,
getTableCellButtonActions, getTableCellButtonActions,
noDataMessage, noDataMessage,
@ -75,12 +79,17 @@ import {
TableWidgetDataKeySettings, TableWidgetDataKeySettings,
TableWidgetSettings TableWidgetSettings
} from '@home/components/widget/lib/table-widget.models'; } from '@home/components/widget/lib/table-widget.models';
import { Overlay } from '@angular/cdk/overlay'; import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer'; import { ResizeObserver } from '@juggle/resize-observer';
import { hidePageSizePixelValue } from '@shared/models/constants'; import { hidePageSizePixelValue } from '@shared/models/constants';
import {
DISPLAY_COLUMNS_PANEL_DATA,
DisplayColumnsPanelComponent
} from '@home/components/widget/lib/display-columns-panel.component';
import { ComponentPortal } from '@angular/cdk/portal';
export interface TimeseriesTableWidgetSettings extends TableWidgetSettings { export interface TimeseriesTableWidgetSettings extends TableWidgetSettings {
showTimestamp: boolean; showTimestamp: boolean;
@ -105,6 +114,8 @@ interface TimeseriesHeader {
dataKey: DataKey; dataKey: DataKey;
sortable: boolean; sortable: boolean;
show: boolean; show: boolean;
columnDefaultVisibility?: boolean;
columnSelectionAvailability?: boolean;
styleInfo: CellStyleInfo; styleInfo: CellStyleInfo;
contentInfo: CellContentInfo; contentInfo: CellContentInfo;
order?: number; order?: number;
@ -131,7 +142,7 @@ interface TimeseriesTableSource {
templateUrl: './timeseries-table-widget.component.html', templateUrl: './timeseries-table-widget.component.html',
styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss'] styleUrls: ['./timeseries-table-widget.component.scss', './table-widget.scss']
}) })
export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit { export class TimeseriesTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -169,6 +180,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private useEntityLabel = false; private useEntityLabel = false;
private dateFormatFilter: string; private dateFormatFilter: string;
private displayedColumns: Array<DisplayColumn[]> = [];
private rowStylesInfo: RowStyleInfo; private rowStylesInfo: RowStyleInfo;
private subscriptions: Subscription[] = []; private subscriptions: Subscription[] = [];
@ -184,6 +197,15 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
} }
}; };
private columnDisplayAction: WidgetAction = {
name: 'entity.columns-to-display',
show: true,
icon: 'view_column',
onAction: ($event) => {
this.editColumnsToDisplay($event);
}
};
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private elementRef: ElementRef, private elementRef: ElementRef,
private overlay: Overlay, private overlay: Overlay,
@ -275,11 +297,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
} }
private initialize() { private initialize() {
this.ctx.widgetActions = [this.searchAction ]; this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length; this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length;
this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true; this.searchAction.show = isDefined(this.settings.enableSearch) ? this.settings.enableSearch : true;
this.columnDisplayAction.show = isDefined(this.settings.enableSelectColumnDisplay) ? this.settings.enableSelectColumnDisplay : true;
this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true; this.displayPagination = isDefined(this.settings.displayPagination) ? this.settings.displayPagination : true;
this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true; this.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : true;
this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true; this.enableStickyAction = isDefined(this.settings.enableStickyAction) ? this.settings.enableStickyAction : true;
@ -365,9 +388,82 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
this.sources.push(source); this.sources.push(source);
} }
} }
this.prepareDisplayedColumn();
this.sources[this.sourceIndex].displayedColumns =
this.displayedColumns[this.sourceIndex].filter(value => value.display).map(value => value.def);
this.updateActiveEntityInfo(); this.updateActiveEntityInfo();
} }
private editColumnsToDisplay($event: Event) {
if ($event) {
$event.stopPropagation();
}
const target = $event.target || $event.currentTarget;
const config = new OverlayConfig();
config.backdropClass = 'cdk-overlay-transparent-backdrop';
config.hasBackdrop = true;
const connectedPosition: ConnectedPosition = {
originX: 'end',
originY: 'bottom',
overlayX: 'end',
overlayY: 'top'
};
config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement)
.withPositions([connectedPosition]);
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const source = this.sources[this.sourceIndex];
this.prepareDisplayedColumn();
const providers: StaticProvider[] = [
{
provide: DISPLAY_COLUMNS_PANEL_DATA,
useValue: {
columns: this.displayedColumns[this.sourceIndex],
columnsUpdated: (newColumns) => {
source.displayedColumns = newColumns.filter(value => value.display).map(value => value.def);
this.clearCache();
}
}
},
{
provide: OverlayRef,
useValue: overlayRef
}
];
const injector = Injector.create({parent: this.viewContainerRef.injector, providers});
overlayRef.attach(new ComponentPortal(DisplayColumnsPanelComponent,
this.viewContainerRef, injector));
this.ctx.detectChanges();
}
private prepareDisplayedColumn() {
if (!this.displayedColumns[this.sourceIndex]) {
this.displayedColumns[this.sourceIndex] = this.sources[this.sourceIndex].displayedColumns.map(value => {
let title = '';
const header = this.sources[this.sourceIndex].header.find(column => column.index.toString() === value);
if (value === '0') {
title = 'Timestamp';
} else if (value === 'actions') {
title = 'Actions';
} else {
title = header.dataKey.name;
}
return {
title,
def: value,
display: header?.columnDefaultVisibility ?? true,
selectable: header?.columnSelectionAvailability ?? true
};
});
}
}
private prepareHeader(datasource: Datasource): TimeseriesHeader[] { private prepareHeader(datasource: Datasource): TimeseriesHeader[] {
const dataKeys = datasource.dataKeys; const dataKeys = datasource.dataKeys;
const latestDataKeys = datasource.latestDataKeys; const latestDataKeys = datasource.latestDataKeys;
@ -377,6 +473,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const keySettings: TableWidgetDataKeySettings = dataKey.settings; const keySettings: TableWidgetDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx');
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx);
const columnSelectionAvailability = getColumnSelectionAvailability(keySettings);
contentInfo.units = dataKey.units; contentInfo.units = dataKey.units;
contentInfo.decimals = dataKey.decimals; contentInfo.decimals = dataKey.decimals;
header.push({ header.push({
@ -386,6 +484,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
styleInfo, styleInfo,
contentInfo, contentInfo,
show: true, show: true,
columnDefaultVisibility,
columnSelectionAvailability,
order: index + 2 order: index + 2
}); });
}); });
@ -396,6 +496,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings; const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx'); const styleInfo = getCellStyleInfo(keySettings, 'value, rowData, ctx');
const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx'); const contentInfo = getCellContentInfo(keySettings, 'value, rowData, ctx');
const columnDefaultVisibility = getColumnDefaultVisibility(keySettings, this.ctx);
const columnSelectionAvailability = getColumnSelectionAvailability(keySettings);
contentInfo.units = dataKey.units; contentInfo.units = dataKey.units;
contentInfo.decimals = dataKey.decimals; contentInfo.decimals = dataKey.decimals;
header.push({ header.push({
@ -405,13 +507,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
styleInfo, styleInfo,
contentInfo, contentInfo,
show: isDefinedAndNotNull(keySettings.show) ? keySettings.show : true, show: isDefinedAndNotNull(keySettings.show) ? keySettings.show : true,
columnDefaultVisibility,
columnSelectionAvailability,
order: isDefinedAndNotNull(keySettings.order) ? keySettings.order : (index + 2) order: isDefinedAndNotNull(keySettings.order) ? keySettings.order : (index + 2)
}); });
}); });
} }
header = header.sort((a, b) => { header = header.sort((a, b) => a.order - b.order);
return a.order - b.order;
});
return header; return header;
} }

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

@ -81,8 +81,8 @@ import { FilterDialogComponent, FilterDialogData } from '@home/components/filter
import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module'; import { basicWidgetConfigComponentsMap } from '@home/components/widget/config/basic/basic-widget-config.module';
import Timeout = NodeJS.Timeout;
import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component'; import { TimewindowConfigData } from '@home/components/widget/config/timewindow-config-panel.component';
import Timeout = NodeJS.Timeout;
const emptySettingsSchema: JsonSchema = { const emptySettingsSchema: JsonSchema = {
type: 'object', type: 'object',
@ -96,7 +96,7 @@ const defaultSettingsForm = [
@Component({ @Component({
selector: 'tb-widget-config', selector: 'tb-widget-config',
templateUrl: './widget-config.component.html', templateUrl: './widget-config.component.html',
styleUrls: ['./widget-config.component.scss', './config/widget-config.scss'], styleUrls: ['./widget-config.component.scss'],
providers: [ providers: [
{ {
provide: NG_VALUE_ACCESSOR, provide: NG_VALUE_ACCESSOR,
@ -143,6 +143,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
@coerceBoolean() @coerceBoolean()
hideToggleHeader = false; hideToggleHeader = false;
@Input()
@coerceBoolean()
isAdd = false;
@Input() disabled: boolean; @Input() disabled: boolean;
@Input() @Input()
@ -399,22 +403,22 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
writeValue(value: WidgetConfigComponentData): void { writeValue(value: WidgetConfigComponentData): void {
this.modelValue = value; this.modelValue = value;
this.widgetType = this.modelValue?.widgetType; this.widgetType = this.modelValue?.widgetType;
this.setupConfig(); this.setupConfig(this.isAdd);
} }
private setupConfig() { private setupConfig(isAdd = false) {
if (this.modelValue) { if (this.modelValue) {
this.destroyBasicModeComponent(); this.destroyBasicModeComponent();
this.removeChangeSubscriptions(); this.removeChangeSubscriptions();
if (this.hasBasicModeDirective && this.widgetConfigMode === WidgetConfigMode.basic) { if (this.hasBasicModeDirective && this.widgetConfigMode === WidgetConfigMode.basic) {
this.setupBasicModeConfig(); this.setupBasicModeConfig(isAdd);
} else { } else {
this.setupDefaultConfig(); this.setupDefaultConfig();
} }
} }
} }
private setupBasicModeConfig() { private setupBasicModeConfig(isAdd = false) {
const componentType = basicWidgetConfigComponentsMap[this.modelValue.basicModeDirective]; const componentType = basicWidgetConfigComponentsMap[this.modelValue.basicModeDirective];
if (!componentType) { if (!componentType) {
this.basicModeDirectiveError = this.translate.instant('widget-config.settings-component-not-found', this.basicModeDirectiveError = this.translate.instant('widget-config.settings-component-not-found',
@ -425,6 +429,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.createBasicModeComponentTimeout = null; this.createBasicModeComponentTimeout = null;
this.basicModeComponentRef = this.basicModeContainer.createComponent(factory); this.basicModeComponentRef = this.basicModeContainer.createComponent(factory);
this.basicModeComponent = this.basicModeComponentRef.instance; this.basicModeComponent = this.basicModeComponentRef.instance;
this.basicModeComponent.isAdd = isAdd;
this.basicModeComponent.widgetConfig = this.modelValue; this.basicModeComponent.widgetConfig = this.modelValue;
this.basicModeComponentChangeSubscription = this.basicModeComponent.widgetConfigChanged.subscribe((data) => { this.basicModeComponentChangeSubscription = this.basicModeComponent.widgetConfigChanged.subscribe((data) => {
this.modelValue = data; this.modelValue = data;
@ -826,7 +831,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
const entityFilter = singleEntityFilterFromDeviceId(deviceId); const entityFilter = singleEntityFilterFromDeviceId(deviceId);
return this.entityService.getEntityKeysByEntityFilter( return this.entityService.getEntityKeysByEntityFilter(
entityFilter, entityFilter,
dataKeyTypes, dataKeyTypes, [EntityType.DEVICE],
{ignoreLoading: true, ignoreErrors: true} {ignoreLoading: true, ignoreErrors: true}
).pipe( ).pipe(
catchError(() => of([])) catchError(() => of([]))
@ -837,7 +842,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
return this.aliasController.getAliasInfo(entityAliasId).pipe( return this.aliasController.getAliasInfo(entityAliasId).pipe(
mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter( mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter, aliasInfo.entityFilter,
dataKeyTypes, dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true} {ignoreLoading: true, ignoreErrors: true}
).pipe( ).pipe(
catchError(() => of([])) catchError(() => of([]))

3
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html

@ -59,8 +59,7 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<tb-template-autocomplete <tb-template-autocomplete
required required allowCreate allowEdit
allowCreate
formControlName="templateId" formControlName="templateId"
[notificationTypes]="ruleNotificationForm.get('triggerType').value"> [notificationTypes]="ruleNotificationForm.get('triggerType').value">
</tb-template-autocomplete> </tb-template-autocomplete>

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

@ -49,8 +49,7 @@
</div> </div>
<div *ngIf="notificationRequestForm.get('useTemplate').value; else scratchTemplate"> <div *ngIf="notificationRequestForm.get('useTemplate').value; else scratchTemplate">
<tb-template-autocomplete <tb-template-autocomplete
required required allowCreate allowEdit
allowCreate
formControlName="templateId" formControlName="templateId"
[notificationTypes]="notificationType.GENERAL"> [notificationTypes]="notificationType.GENERAL">
</tb-template-autocomplete> </tb-template-autocomplete>

67
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.html

@ -15,55 +15,48 @@
limitations under the License. limitations under the License.
--> -->
<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys" <div class="mat-content tb-rulechain" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
[cheatSheet]="cheatSheetComponent" [cheatSheet]="cheatSheetComponent" fxLayout="column">
fxLayout="column" class="tb-rulechain">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-rulechain-container" fxFlex fxLayout="column"> <section class="tb-rulechain-container" fxFlex fxLayout="column">
<div class="tb-rulechain-layout" fxFlex fxLayout="row"> <div class="tb-rulechain-layout" fxFlex fxLayout="row">
<section fxLayout="row"
class="tb-header-buttons tb-library-open" [fxShow]="!isLibraryOpen">
<button color="primary"
mat-mini-fab
class="tb-btn-header tb-btn-open-library"
(click)="isLibraryOpen = true"
matTooltip="{{ 'rulenode.open-node-library' | translate }}"
matTooltipPosition="above">
<mat-icon>menu</mat-icon>
</button>
</section>
<mat-drawer-container style="width: 100%; height: 100%;"> <mat-drawer-container style="width: 100%; height: 100%;">
<mat-drawer class="tb-rulechain-library mat-elevation-z4" <mat-drawer class="tb-rulechain-library mat-elevation-z4"
disableClose="true" disableClose="true"
mode="side" mode="side"
[opened]="isLibraryOpen" #drawer
opened
position="start" position="start"
fxLayout="column"> fxLayout="column">
<mat-toolbar color="primary" class="tb-dark"> <mat-toolbar color="primary" class="tb-dark">
<tb-rule-chain-select
fxFlex
*ngIf="!isImport"
[ruleChainType]="ruleChainType"
[disabled]="isDirtyValue"
[(ngModel)]="ruleChain.id.id"
(ngModelChange)="currentRuleChainIdChanged(ruleChain.id?.id)">
</tb-rule-chain-select>
</mat-toolbar>
<mat-toolbar>
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools">
<button mat-icon-button class="tb-small"
matTooltip="{{'rulenode.search' | translate}}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<mat-form-field fxFlex class="tb-appearance-transparent"> <mat-form-field fxFlex class="tb-appearance-transparent">
<button mat-icon-button matPrefix class="tb-small"
matTooltip="{{'rulenode.search' | translate}}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<input #ruleNodeSearchInput matInput <input #ruleNodeSearchInput matInput
[(ngModel)]="ruleNodeTypeSearch" [(ngModel)]="ruleNodeTypeSearch"
placeholder="{{'rulenode.search' | translate}}"/> placeholder="{{'rulenode.search' | translate}}"/>
<button mat-icon-button matSuffix class="tb-small"
[fxShow]="ruleNodeTypeSearch !== ''"
(click)="ruleNodeTypeSearch = ''; updateRuleChainLibrary()"
matTooltip="{{'action.clear-search' | translate}}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</mat-form-field> </mat-form-field>
<button mat-icon-button class="tb-small"
[fxShow]="ruleNodeTypeSearch !== ''"
(click)="ruleNodeTypeSearch = ''; updateRuleChainLibrary()"
matTooltip="{{'action.clear-search' | translate}}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
<button mat-icon-button class="tb-small"
(click)="isLibraryOpen = false"
matTooltip="{{'action.close' | translate}}"
matTooltipPosition="above">
<mat-icon>chevron_left</mat-icon>
</button>
</div> </div>
</mat-toolbar> </mat-toolbar>
<div class="tb-rulechain-library-panel-group"> <div class="tb-rulechain-library-panel-group">
@ -164,6 +157,14 @@
</tb-details-panel> </tb-details-panel>
</mat-drawer> </mat-drawer>
<mat-drawer-content class="tb-rulechain-graph-content"> <mat-drawer-content class="tb-rulechain-graph-content">
<button color="primary"
mat-mini-fab
class="tb-library-node-btn"
(click)="drawer.toggle();"
matTooltip="{{ (drawer.opened ? 'rulenode.close-node-library' : 'rulenode.open-node-library') | translate }}"
matTooltipPosition="above">
<mat-icon class="tb-library-node-btn-icon" [ngClass]="{'tb-library-node-btn-icon-toggled' : drawer.opened}">chevron_right</mat-icon>
</button>
<button #versionControlButton <button #versionControlButton
*ngIf="!isImport" *ngIf="!isImport"
color="primary" color="primary"

50
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.scss

@ -54,20 +54,43 @@
z-index: 2; z-index: 2;
} }
section.tb-header-buttons.tb-library-open { .tb-library-node-btn {
width: 20px;
height: 90px;
position: absolute; position: absolute;
top: 0; top: 50%;
left: 0; z-index: 1000;
z-index: 2; opacity: 0.5;
pointer-events: none; border-radius: 0 15px 15px 0 !important;
.mdc-fab.tb-btn-open-library { transform: translateY(-50%);
top: 0; &-icon {
transition: transform 0.4s !important;
&-toggled {
transform: rotateZ(180deg);
}
}
&:before {
width: 20px;
height: 30px;
top: -28px;
left: 0;
content: "";
pointer-events: none;
border-radius: 0 0 0 15px;
box-shadow: -10px 0px 0 0px #305680;
border: none;
}
&:after {
position: absolute;
width: 20px;
height: 30px;
bottom: -28px;
left: 0; left: 0;
width: 36px; content: "";
height: 36px; pointer-events: none;
margin: 4px 0 0 4px; border-top-left-radius: 15px;
line-height: 36px; box-shadow: -10px 0px 0px 0px #305680;
opacity: .5; border: none;
} }
} }
@ -77,8 +100,7 @@
min-width: 250px; min-width: 250px;
.mat-toolbar { .mat-toolbar {
height: 48px; height: min-content;
min-height: 48px;
padding: 0; padding: 0;
.mat-toolbar-tools { .mat-toolbar-tools {

21
ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts

@ -15,7 +15,9 @@
/// ///
import { import {
AfterViewChecked,
AfterViewInit, AfterViewInit,
ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
@ -90,6 +92,7 @@ import { MatMiniFabButton } from '@angular/material/button';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { VersionControlComponent } from '@home/components/vc/version-control.component'; import { VersionControlComponent } from '@home/components/vc/version-control.component';
import { ComponentClusteringMode } from '@shared/models/component-descriptor.models'; import { ComponentClusteringMode } from '@shared/models/component-descriptor.models';
import { MatDrawer } from '@angular/material/sidenav';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@Component({ @Component({
@ -99,7 +102,7 @@ import Timeout = NodeJS.Timeout;
encapsulation: ViewEncapsulation.None encapsulation: ViewEncapsulation.None
}) })
export class RuleChainPageComponent extends PageComponent export class RuleChainPageComponent extends PageComponent
implements AfterViewInit, OnInit, OnDestroy, HasDirtyFlag, ISearchableComponent { implements AfterViewInit, OnInit, OnDestroy, HasDirtyFlag, ISearchableComponent, AfterViewChecked {
get isDirty(): boolean { get isDirty(): boolean {
return this.isDirtyValue || this.isImport; return this.isDirtyValue || this.isImport;
@ -121,6 +124,8 @@ export class RuleChainPageComponent extends PageComponent
@ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger; @ViewChild('ruleChainMenuTrigger', {static: true}) ruleChainMenuTrigger: MatMenuTrigger;
@ViewChild('drawer') drawer: MatDrawer;
eventTypes = EventType; eventTypes = EventType;
debugEventTypes = DebugEventType; debugEventTypes = DebugEventType;
@ -159,7 +164,6 @@ export class RuleChainPageComponent extends PageComponent
hotKeys: Hotkey[] = []; hotKeys: Hotkey[] = [];
enableHotKeys = true; enableHotKeys = true;
isLibraryOpen = true;
ruleNodeSearch = ''; ruleNodeSearch = '';
ruleNodeTypeSearch = ''; ruleNodeTypeSearch = '';
@ -266,6 +270,7 @@ export class RuleChainPageComponent extends PageComponent
private popoverService: TbPopoverService, private popoverService: TbPopoverService,
private renderer: Renderer2, private renderer: Renderer2,
private viewContainerRef: ViewContainerRef, private viewContainerRef: ViewContainerRef,
private changeDetector: ChangeDetectorRef,
public dialog: MatDialog, public dialog: MatDialog,
public dialogService: DialogService, public dialogService: DialogService,
public fb: UntypedFormBuilder) { public fb: UntypedFormBuilder) {
@ -281,6 +286,10 @@ export class RuleChainPageComponent extends PageComponent
ngOnInit() { ngOnInit() {
} }
ngAfterViewChecked(){
this.changeDetector.detectChanges();
}
ngAfterViewInit() { ngAfterViewInit() {
fromEvent(this.ruleNodeSearchInputField.nativeElement, 'keyup') fromEvent(this.ruleNodeSearchInputField.nativeElement, 'keyup')
.pipe( .pipe(
@ -299,6 +308,14 @@ export class RuleChainPageComponent extends PageComponent
this.rxSubscription.unsubscribe(); this.rxSubscription.unsubscribe();
} }
currentRuleChainIdChanged(ruleChainId: string) {
if (this.ruleChainType === RuleChainType.CORE) {
this.router.navigateByUrl(`ruleChains/${ruleChainId}`);
} else {
this.router.navigateByUrl(`edgeManagement/ruleChains/${ruleChainId}`);
}
}
onSearchTextUpdated(searchText: string) { onSearchTextUpdated(searchText: string) {
this.ruleNodeSearch = searchText; this.ruleNodeSearch = searchText;
this.updateRuleNodesHighlight(); this.updateRuleNodesHighlight();

8
ui-ngx/src/app/shared/components/notification/template-autocomplete.component.html

@ -29,6 +29,14 @@
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
<button *ngIf="selectTemplateFormGroup.get('templateName').value && !disabled && allowEdit"
type="button"
matSuffix mat-icon-button aria-label="Edit"
matTooltip="{{ 'notification.edit-notification-template' | translate }}"
matTooltipPosition="above"
(click)="editTemplate($event)">
<mat-icon class="material-icons">edit</mat-icon>
</button>
<button #createTemplateButton <button #createTemplateButton
mat-button color="primary" matSuffix mat-button color="primary" matSuffix
*ngIf="allowCreate && !selectTemplateFormGroup.get('templateName').value && !disabled" *ngIf="allowCreate && !selectTemplateFormGroup.get('templateName').value && !disabled"

28
ui-ngx/src/app/shared/components/notification/template-autocomplete.component.ts

@ -17,7 +17,7 @@
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -66,6 +66,10 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
@coerceBoolean() @coerceBoolean()
allowCreate = false; allowCreate = false;
@Input()
@coerceBoolean()
allowEdit = false;
@Input() @Input()
disabled: boolean; disabled: boolean;
@ -196,18 +200,34 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
}, 0); }, 0);
} }
editTemplate($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.notificationService.getNotificationTemplateById(this.modelValue.id).subscribe(
(template) => {
this.openNotificationTemplateDialog({template});
}
);
}
createTemplate($event: Event, button: MatButton) { createTemplate($event: Event, button: MatButton) {
if ($event) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }
button._elementRef.nativeElement.blur(); button._elementRef.nativeElement.blur();
this.openNotificationTemplateDialog({
isAdd: true,
predefinedType: this.notificationTypes
});
}
private openNotificationTemplateDialog(dialogData?: TemplateNotificationDialogData) {
this.dialog.open<TemplateNotificationDialogComponent, TemplateNotificationDialogData, this.dialog.open<TemplateNotificationDialogComponent, TemplateNotificationDialogData,
NotificationTemplate>(TemplateNotificationDialogComponent, { NotificationTemplate>(TemplateNotificationDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: dialogData
predefinedType: this.notificationTypes
}
}).afterClosed() }).afterClosed()
.subscribe((res) => { .subscribe((res) => {
if (res) { if (res) {

27
ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.html

@ -0,0 +1,27 @@
<!--
Copyright © 2016-2023 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<mat-select fxFlex
class="tb-rule-chain-select"
[required]="required"
[disabled]="disabled"
[(ngModel)]="ruleChainId"
(ngModelChange)="ruleChainIdChanged()">
<mat-option *ngFor="let ruleChain of ruleChains$ | async" [value]="ruleChain.id.id">
{{ruleChain.name}}
</mat-option>
</mat-select>

26
ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.scss

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:host {
min-width: 52px;
width: 100%;
padding: 0 6px;
.tb-rule-chain-select {
display: flex;
height: 48px;
min-height: 100%;
pointer-events: all;
}
}

109
ui-ngx/src/app/shared/components/rule-chain/rule-chain-select.component.ts

@ -0,0 +1,109 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable } from 'rxjs';
import { PageLink } from '@shared/models/page/page-link';
import { map, share } from 'rxjs/operators';
import { PageData } from '@shared/models/page/page-data';
import { Store } from '@ngrx/store';
import { AppState } from '@app/core/core.state';
import { TooltipPosition } from '@angular/material/tooltip';
import { RuleChain, RuleChainType } from '@shared/models/rule-chain.models';
import { RuleChainService } from '@core/http/rule-chain.service';
import { isDefinedAndNotNull } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { Direction } from '@shared/models/page/sort-order';
@Component({
selector: 'tb-rule-chain-select',
templateUrl: './rule-chain-select.component.html',
styleUrls: ['./rule-chain-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => RuleChainSelectComponent),
multi: true
}]
})
export class RuleChainSelectComponent implements ControlValueAccessor, OnInit {
@Input()
tooltipPosition: TooltipPosition = 'above';
@Input()
@coerceBoolean()
required: boolean;
@Input()
@coerceBoolean()
disabled: boolean;
@Input()
ruleChainType: RuleChainType = RuleChainType.CORE;
ruleChains$: Observable<Array<RuleChain>>;
ruleChainId: string | null;
private propagateChange = (v: any) => { };
constructor(private ruleChainService: RuleChainService) {
}
ngOnInit() {
const pageLink = new PageLink(100, 0, null, {
property: 'name',
direction: Direction.ASC
});
this.ruleChains$ = this.getRuleChains(pageLink).pipe(
map((pageData) => pageData.data),
share()
);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
writeValue(value: string | null): void {
if (isDefinedAndNotNull(value)) {
this.ruleChainId = value;
}
}
ruleChainIdChanged() {
this.updateView();
}
private updateView() {
this.propagateChange(this.ruleChainId);
}
private getRuleChains(pageLink: PageLink): Observable<PageData<RuleChain>> {
return this.ruleChainService.getRuleChains(pageLink, this.ruleChainType, {ignoreLoading: true});
}
}

7
ui-ngx/src/app/shared/shared.module.ts

@ -198,6 +198,7 @@ import {
import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component'; import { ColorPickerComponent } from '@shared/components/color-picker/color-picker.component';
import { ShortNumberPipe } from '@shared/pipe/short-number.pipe'; import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent } from '@shared/components/toggle-header.component'; import { ToggleHeaderComponent } from '@shared/components/toggle-header.component';
import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService) => markedOptionsService; export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService) => markedOptionsService;
@ -373,7 +374,8 @@ export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective, GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective, GtMdLgShowHideDirective,
ColorPickerComponent, ColorPickerComponent,
ToggleHeaderComponent ToggleHeaderComponent,
RuleChainSelectComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -606,7 +608,8 @@ export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective, GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective, GtMdLgShowHideDirective,
ColorPickerComponent, ColorPickerComponent,
ToggleHeaderComponent ToggleHeaderComponent,
RuleChainSelectComponent
] ]
}) })
export class SharedModule { } export class SharedModule { }

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

@ -1096,6 +1096,7 @@
"datakey": { "datakey": {
"settings": "Settings", "settings": "Settings",
"advanced": "Advanced", "advanced": "Advanced",
"key": "Key",
"label": "Label", "label": "Label",
"color": "Color", "color": "Color",
"units": "Special symbol to show next to value", "units": "Special symbol to show next to value",
@ -3556,6 +3557,7 @@
"events": "Events", "events": "Events",
"search": "Search nodes", "search": "Search nodes",
"open-node-library": "Open node library", "open-node-library": "Open node library",
"close-node-library": "Close node library",
"add": "Add rule node", "add": "Add rule node",
"name": "Name", "name": "Name",
"name-required": "Name is required.", "name-required": "Name is required.",
@ -4332,6 +4334,7 @@
"title-tooltip": "Title Tooltip", "title-tooltip": "Title Tooltip",
"general-settings": "General settings", "general-settings": "General settings",
"display-title": "Display widget title", "display-title": "Display widget title",
"card-title": "Card title",
"drop-shadow": "Drop shadow", "drop-shadow": "Drop shadow",
"enable-fullscreen": "Enable fullscreen", "enable-fullscreen": "Enable fullscreen",
"background-color": "Background color", "background-color": "Background color",
@ -4386,6 +4389,7 @@
"delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?", "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?",
"title-icon": "Title icon", "title-icon": "Title icon",
"display-icon": "Display title icon", "display-icon": "Display title icon",
"card-icon": "Card icon",
"icon-color": "Icon color", "icon-color": "Icon color",
"icon-size": "Icon size", "icon-size": "Icon size",
"advanced-settings": "Advanced settings", "advanced-settings": "Advanced settings",
@ -5384,7 +5388,11 @@
"allow-alarms-ack": "Allow alarms acknowledgment", "allow-alarms-ack": "Allow alarms acknowledgment",
"allow-alarms-clear": "Allow alarms clear", "allow-alarms-clear": "Allow alarms clear",
"display-alarm-activity": "Display alarm activity", "display-alarm-activity": "Display alarm activity",
"allow-alarms-assign": "Allow alarms assignment" "allow-alarms-assign": "Allow alarms assignment",
"columns": "Columns",
"remove-column": "Remove column",
"add-column": "Add column",
"no-columns": "No columns configured"
}, },
"value-source": { "value-source": {
"value-source": "Value source", "value-source": "Value source",

162
ui-ngx/src/styles.scss

@ -1183,4 +1183,166 @@ mat-label {
.mat-expansion-panel { .mat-expansion-panel {
color: inherit; color: inherit;
} }
// Widget config
.tb-widget-config-panel {
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04);
border-radius: 4px;
padding: 16px;
gap: 16px;
display: flex;
flex-direction: column;
color: rgba(0, 0, 0, 0.87);
letter-spacing: 0.15px;
position: relative;
&.no-padding-bottom {
padding-bottom: 0;
}
&.stroked {
box-shadow: none;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
}
.mat-expansion-panel {
&.tb-settings {
box-shadow: none;
.mat-content {
overflow: visible;
}
.mat-expansion-panel-header {
font-weight: 500;
font-size: 16px;
line-height: 24px;
letter-spacing: 0.25px;
padding: 0;
.mat-content {
flex: 0;
white-space: nowrap;
}
&:hover {
background: none;
}
.mat-expansion-indicator {
height: 32px;
padding: 2px;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
> .mat-expansion-panel-content {
> .mat-expansion-panel-body {
padding: 0;
}
}
.tb-json-object-panel, .tb-css-content-panel {
margin: 0 0 8px;
}
}
.mat-expansion-panel-content {
font: inherit;
}
}
.mat-slide {
margin: 8px 0;
.mdc-form-field>label {
font-weight: 400;
font-size: 16px;
line-height: 24px;
margin-left: 12px;
}
}
}
.tb-widget-config-panel-title {
font-weight: 500;
font-size: 16px;
}
.tb-widget-config-panel-hint {
font-size: 12px;
color: #808080;
}
.tb-widget-config-row {
height: 56px;
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
padding-left: 16px;
padding-right: 12px;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
&.same-padding {
padding-right: 16px;
}
&.space-between {
justify-content: space-between;
}
.mat-divider-vertical {
height: 56px;
}
.mat-mdc-form-field {
width: 80px;
}
}
.tb-widget-config-row .mat-mdc-form-field, .mat-mdc-form-field.tb-inline-field {
&.mat-form-field-appearance-fill {
.mdc-text-field--filled:not(.mdc-text-field--disabled) {
&:before {
opacity: 0;
}
.mdc-line-ripple::before {
border-bottom-color: rgba(0, 0, 0, 0.12);
}
}
.mat-mdc-form-field-focus-overlay {
opacity: 0;
}
}
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-right: 12px;
padding-left: 12px;
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
border-color: rgba(0, 0, 0, 0.12);
}
}
.mat-mdc-form-field-infix {
padding-top: 7px;
padding-bottom: 7px;
min-height: 38px;
width: auto;
.mdc-text-field__input, .mat-mdc-select {
font-weight: 400;
font-size: 14px;
line-height: 20px;
}
}
}
}
&.center {
.mat-mdc-text-field-wrapper {
.mat-mdc-form-field-infix {
.mdc-text-field__input {
text-align: center;
}
}
}
}
&.number {
.mat-mdc-text-field-wrapper {
padding-right: 4px;
.mat-mdc-form-field-infix {
input.mdc-text-field__input[type=number]::-webkit-inner-spin-button,
input.mdc-text-field__input[type=number]::-webkit-outer-spin-button {
opacity: 1;
}
}
}
}
}
} }

Loading…
Cancel
Save