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",
"hasBasicMode": true,
"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": "",
"settingsDirective": "tb-entities-table-widget-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.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationId extends UUIDBased implements EntityId {
super(id);
}
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION", allowableValues = "NOTIFICATION")
@Override
public EntityType getEntityType() {
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.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationRequestId extends UUIDBased implements EntityId {
super(id);
}
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_REQUEST", allowableValues = "NOTIFICATION_REQUEST")
@Override
public EntityType getEntityType() {
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.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationRuleId extends UUIDBased implements EntityId {
super(id);
}
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_RULE", allowableValues = "NOTIFICATION_RULE")
@Override
public EntityType getEntityType() {
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.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationTargetId extends UUIDBased implements EntityId {
super(id);
}
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_TARGET", allowableValues = "NOTIFICATION_TARGET")
@Override
public EntityType getEntityType() {
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.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@ -28,6 +29,7 @@ public class NotificationTemplateId extends UUIDBased implements EntityId {
super(id);
}
@ApiModelProperty(position = 2, required = true, value = "string", example = "NOTIFICATION_TEMPLATE", allowableValues = "NOTIFICATION_TEMPLATE")
@Override
public EntityType getEntityType() {
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.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.thingsboard.server.common.data.EntityType;
import java.util.UUID;
@ -34,6 +35,7 @@ public class QueueId extends UUIDBased implements EntityId {
return new QueueId(UUID.fromString(queueId));
}
@ApiModelProperty(position = 2, required = true, value = "string", example = "QUEUE", allowableValues = "QUEUE")
@Override
public EntityType getEntityType() {
return EntityType.QUEUE;

12
pom.xml

@ -111,7 +111,7 @@
<aws.sdk.version>1.11.747</aws.sdk.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-->
<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>
<ua-parser.version>1.5.4</ua-parser.version>
<commons-beanutils.version>1.9.4</commons-beanutils.version>
@ -1836,6 +1836,16 @@
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-servicebus</artifactId>
<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>
<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) {
return of([]);
}
@ -832,7 +833,7 @@ export class EntityService {
entitiesKeysByQuery$ = of({
attribute: [],
timeseries: [],
entityTypes: [],
entityTypes: entityTypes || [],
});
}
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;
newDatasource.dataKeys = [];
dataKeys.forEach(dataKey => {
newDatasource.dataKeys.push(this.convertDataKeyFromWidgetType(widgetTypeDescriptor, config, dataKey));
});
if (widgetTypeDescriptor.type === widgetType.alarm) {
dataKeys.forEach(dataKey => {
const newDataKey = deepClone(dataKey);
newDataKey.funcBody = null;
newDataKey.type = DataKeyType.alarm;
newDatasource.dataKeys.push(newDataKey);
});
}
}
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) {
if (!state.layouts) {
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 GatewayStatisticsComponent from '@shared/components/device/gateway-statistics.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';
@ -433,6 +433,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/time/quick-time-interval.component': QuickTimeIntervalComponent,
'@shared/components/dashboard-select.component': DashboardSelectComponent,
'@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.component': DatetimeComponent,
'@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"
[widgetConfigMode]="widgetConfigMode"
[hideHeader]="widgetConfigMode === widgetConfigModes.basic"
isAdd
formControlName="widgetConfig">
</tb-widget-config>
<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 {
WidgetActionsPanelComponent
} 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({
declarations: [
WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent
SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
],
imports: [
CommonModule,
@ -38,12 +46,16 @@ import {
],
exports: [
WidgetActionsPanelComponent,
SimpleCardBasicConfigComponent
SimpleCardBasicConfigComponent,
EntitiesTableBasicConfigComponent,
DataKeyRowComponent,
DataKeysPanelComponent
]
})
export class BasicWidgetConfigModule {
}
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 {
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation
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-simple-card-basic-config',
templateUrl: './simple-card-basic-config.component.html',
styleUrls: ['../basic-config.scss', '../../widget-config.scss']
styleUrls: ['../basic-config.scss']
})
export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
@ -46,14 +48,19 @@ export class SimpleCardBasicConfigComponent extends BasicWidgetConfigComponent {
simpleCardWidgetConfigForm: UntypedFormGroup;
constructor(protected store: Store<AppState>,
protected widgetConfigComponent: WidgetConfigComponent,
private fb: UntypedFormBuilder) {
super(store);
super(store, widgetConfigComponent);
}
protected configForm(): UntypedFormGroup {
return this.simpleCardWidgetConfigForm;
}
protected setupDefaults(configData: WidgetConfigComponentData) {
this.setupDefaultDatasource(configData, [{ name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries }]);
}
protected onConfigSet(configData: WidgetConfigComponentData) {
this.simpleCardWidgetConfigForm = this.fb.group({
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({
selector: 'tb-widget-actions-panel',
templateUrl: './widget-actions-panel.component.html',
styleUrls: ['../../widget-config.scss'],
styleUrls: [],
providers: [
{
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 alarmKeys: Array<DataKey>;
private functionTypeKeys: Array<DataKey>;
filteredKeys: Observable<Array<string>>;
private latestKeySearchResult: Array<string> = null;
@ -183,6 +184,13 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
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 ||
this.dataKeySettingsDirective && this.dataKeySettingsDirective.length) {
this.displayAdvanced = true;
@ -396,6 +404,8 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con
let fetchObservable: Observable<Array<DataKey>>;
if (this.modelValue.type === DataKeyType.alarm) {
fetchObservable = of(this.alarmKeys);
} else if (this.modelValue.type === DataKeyType.function) {
fetchObservable = of(this.functionTypeKeys);
} else {
if (this.deviceId || this.entityAliasId) {
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 {
display: none;
}
}
.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip {
overflow: hidden;
line-height: 20px;
height: 32px;
.mat-mdc-chip.mat-mdc-standard-chip.tb-datakey-chip {
overflow: hidden;
line-height: 20px;
height: 32px;
&.mdc-evolution-chip--with-trailing-action {
.mdc-evolution-chip__action--primary {
padding-left: 4px;
padding-right: 12px;
}
&.mdc-evolution-chip--with-trailing-action {
.mdc-evolution-chip__action--primary {
padding-left: 4px;
padding-right: 12px;
}
}
.mat-mdc-chip-action {
.mat-mdc-chip-action {
overflow: hidden;
.mat-mdc-chip-action-label {
overflow: hidden;
.mat-mdc-chip-action-label {
overflow: hidden;
}
}
.tb-attribute-chip {
max-width: 100%;
color: rgb(66, 66, 66);
.tb-chip-drag-handle {
padding: 3px;
height: 24px;
cursor: move;
mat-icon {
pointer-events: none;
}
}
.tb-attribute-chip {
max-width: 100%;
color: rgb(66, 66, 66);
.tb-chip-drag-handle {
padding: 3px;
height: 24px;
cursor: move;
mat-icon {
pointer-events: none;
}
.tb-chip-labels {
display: flex;
flex-direction: row;
align-items: center;
min-width: 0;
background: rgba(0, 0, 0, 0.04);
border-radius: 100px;
padding: 2px 10px;
.tb-chip-label {
font-weight: normal;
font-size: 14px;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.mat-icon.tb-datakey-icon {
margin-right: 4px;
margin-left: 4px;
}
.tb-agg-func {
font-style: italic;
color: #0c959c;
}
}
.tb-chip-labels {
display: flex;
flex-direction: row;
align-items: center;
min-width: 0;
background: rgba(0, 0, 0, 0.04);
border-radius: 100px;
padding: 2px 10px;
.tb-chip-label {
font-weight: normal;
font-size: 14px;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.mat-icon.tb-datakey-icon {
margin-right: 4px;
margin-left: 4px;
}
.tb-chip-separator {
white-space: pre;
.tb-agg-func {
font-style: italic;
color: #0c959c;
}
}
.mat-mdc-chip-remove.mat-mdc-icon-button {
color: inherit;
opacity: inherit;
.tb-chip-separator {
white-space: pre;
}
}
&.tb-datakey-chip-dnd-placeholder {
min-width: 120px;
border: 2px dashed rgba(0, 0, 0, 0.2);
}
&.tb-chip-dragging {
display: none;
.mat-mdc-chip-remove.mat-mdc-icon-button {
color: inherit;
opacity: inherit;
}
}
&.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 {
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 {
display: block;
}
display: block;
}
}
}

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

@ -61,7 +61,7 @@
</mat-form-field>
</ng-template>
</section>
<section fxLayout="column" fxLayoutAlign="stretch" fxFlex>
<section *ngIf="!hideDataKeys" fxLayout="column" fxLayoutAlign="stretch" fxFlex>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[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;
}
public get hideDataKeys(): boolean {
return this.datasourcesComponent?.hideDataKeys;
}
@Input()
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({
selector: 'tb-datasources',
templateUrl: './datasources.component.html',
styleUrls: ['./datasources.component.scss', './widget-config.scss'],
styleUrls: ['./datasources.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -104,6 +104,10 @@ export class DatasourcesComponent implements ControlValueAccessor, OnInit, Valid
@coerceBoolean()
hideDataKeyDecimals = false;
@Input()
@coerceBoolean()
hideDataKeys = false;
@Input()
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({
selector: 'tb-timewindow-config-panel',
templateUrl: './timewindow-config-panel.component.html',
styleUrls: ['./widget-config.scss'],
styleUrls: [],
providers: [
{
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 { AppState } from '@core/core.state';
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 interface IBasicWidgetConfigComponent {
isAdd: boolean;
widgetConfig: WidgetConfigComponentData;
widgetConfigChanged: Observable<WidgetConfigComponentData>;
validateConfig(): boolean;
@ -40,6 +43,8 @@ export interface IBasicWidgetConfigComponent {
export abstract class BasicWidgetConfigComponent extends PageComponent implements
IBasicWidgetConfigComponent, OnInit, AfterViewInit {
isAdd = false;
basicMode = WidgetConfigMode.basic;
widgetConfigValue: WidgetConfigComponentData;
@ -56,7 +61,8 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
widgetConfigChangedEmitter = new EventEmitter<WidgetConfigComponentData>();
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);
}
@ -65,12 +71,15 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
ngAfterViewInit(): void {
setTimeout(() => {
if (!this.validateConfig()) {
this.onConfigChanged(this.prepareOutputConfig(this.configForm().value));
this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue()));
}
}, 0);
}
protected setupConfig(widgetConfig: WidgetConfigComponentData) {
if (this.isAdd) {
this.setupDefaults(widgetConfig);
}
this.onConfigSet(widgetConfig);
this.updateValidators(false);
for (const trigger of this.validatorTriggers()) {
@ -83,11 +92,13 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
this.updateValidators(true, trigger);
});
}
this.configForm().valueChanges.subscribe((updated: any) => {
this.onConfigChanged(this.prepareOutputConfig(updated));
this.configForm().valueChanges.subscribe(() => {
this.onConfigChanged(this.prepareOutputConfig(this.configForm().getRawValue()));
});
}
protected setupDefaults(configData: WidgetConfigComponentData) {}
protected updateValidators(emitEvent: boolean, trigger?: string) {
}
@ -108,6 +119,41 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
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 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.
-->
<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 }}">
</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({
selector: 'tb-widget-units',
templateUrl: './widget-units.component.html',
styleUrls: ['./widget-config.scss'],
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -43,6 +43,7 @@ export class WidgetUnitsComponent implements ControlValueAccessor, OnInit {
ngOnInit() {
this.unitsFormControl = this.fb.control('', []);
this.unitsFormControl.valueChanges.subscribe(val => this.propagateChange(val));
}
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({
selector: 'tb-simple-card-widget-settings',
templateUrl: './simple-card-widget-settings.component.html',
styleUrls: ['../../../config/widget-config.scss']
styleUrls: []
})
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>
</mat-expansion-panel>
</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>

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,
cellStyleFunction: '',
useCellContentFunction: false,
cellContentFunction: ''
cellContentFunction: '',
defaultColumnVisibility: 'visible',
columnSelectionToDisplay: 'enabled'
};
}
@ -53,6 +55,8 @@ export class TimeseriesTableKeySettingsComponent extends WidgetSettingsComponent
cellStyleFunction: [settings.cellStyleFunction, [Validators.required]],
useCellContentFunction: [settings.useCellContentFunction, []],
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>
</mat-expansion-panel>
</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>

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,
cellStyleFunction: '',
useCellContentFunction: false,
cellContentFunction: ''
cellContentFunction: '',
defaultColumnVisibility: 'visible',
columnSelectionToDisplay: 'enabled'
};
}
@ -56,6 +58,8 @@ export class TimeseriesTableLatestKeySettingsComponent extends WidgetSettingsCom
cellStyleFunction: [settings.cellStyleFunction, [Validators.required]],
useCellContentFunction: [settings.useCellContentFunction, []],
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">
{{ 'widgets.table.enable-search' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="enableSelectColumnDisplay">
{{ 'widgets.table.enable-select-column-display' | translate }}
</mat-checkbox>
</section>
<section fxLayout="column" fxFlex>
<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 {
return {
enableSearch: true,
enableSelectColumnDisplay: true,
enableStickyHeader: true,
enableStickyAction: true,
reserveSpaceForHiddenAction: 'true',
@ -59,6 +60,7 @@ export class TimeseriesTableWidgetSettingsComponent extends WidgetSettingsCompon
protected onSettingsSet(settings: WidgetSettings) {
this.timeseriesTableWidgetSettingsForm = this.fb.group({
enableSearch: [settings.enableSearch, []],
enableSelectColumnDisplay: [settings.enableSelectColumnDisplay, []],
enableStickyHeader: [settings.enableStickyHeader, []],
enableStickyAction: [settings.enableStickyAction, []],
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) => {
return this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter,
dataKeyTypes,
dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true}
).pipe(
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 { 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 { Store } from '@ngrx/store';
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 { EntityService } from '@core/http/entity.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { EntityType } from '@shared/models/entity-type.models';
@Component({
selector: 'tb-device-key-autocomplete',
@ -199,7 +206,7 @@ export class DeviceKeyAutocompleteComponent extends PageComponent implements OnI
mergeMap((aliasInfo) => {
return this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter,
dataKeyTypes,
dataKeyTypes, [EntityType.DEVICE],
{ignoreLoading: true, ignoreErrors: true}
).pipe(
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) => {
return this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter,
dataKeyTypes,
dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true}
).pipe(
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 {
enableSearch: boolean;
enableSelectColumnDisplay: boolean;
enableStickyAction: boolean;
enableStickyHeader: boolean;
displayPagination: boolean;

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

@ -19,9 +19,12 @@ import {
ChangeDetectorRef,
Component,
ElementRef,
Injector,
Input,
OnDestroy,
OnInit,
QueryList,
StaticProvider,
ViewChild,
ViewChildren,
ViewContainerRef
@ -64,8 +67,9 @@ import {
CellStyleInfo,
checkHasActions,
constructTableCssString,
DisplayColumn,
getCellContentInfo,
getCellStyleInfo,
getCellStyleInfo, getColumnDefaultVisibility, getColumnSelectionAvailability,
getRowStyleInfo,
getTableCellButtonActions,
noDataMessage,
@ -75,12 +79,17 @@ import {
TableWidgetDataKeySettings,
TableWidgetSettings
} 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 { DatePipe } from '@angular/common';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ResizeObserver } from '@juggle/resize-observer';
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 {
showTimestamp: boolean;
@ -105,6 +114,8 @@ interface TimeseriesHeader {
dataKey: DataKey;
sortable: boolean;
show: boolean;
columnDefaultVisibility?: boolean;
columnSelectionAvailability?: boolean;
styleInfo: CellStyleInfo;
contentInfo: CellContentInfo;
order?: number;
@ -131,7 +142,7 @@ interface TimeseriesTableSource {
templateUrl: './timeseries-table-widget.component.html',
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()
ctx: WidgetContext;
@ -169,6 +180,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
private useEntityLabel = false;
private dateFormatFilter: string;
private displayedColumns: Array<DisplayColumn[]> = [];
private rowStylesInfo: RowStyleInfo;
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>,
private elementRef: ElementRef,
private overlay: Overlay,
@ -275,11 +297,12 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
}
private initialize() {
this.ctx.widgetActions = [this.searchAction ];
this.ctx.widgetActions = [this.searchAction, this.columnDisplayAction];
this.setCellButtonAction = !!this.ctx.actionsApi.getActionDescriptors('actionCellButton').length;
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.enableStickyHeader = isDefined(this.settings.enableStickyHeader) ? this.settings.enableStickyHeader : 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.prepareDisplayedColumn();
this.sources[this.sourceIndex].displayedColumns =
this.displayedColumns[this.sourceIndex].filter(value => value.display).map(value => value.def);
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[] {
const dataKeys = datasource.dataKeys;
const latestDataKeys = datasource.latestDataKeys;
@ -377,6 +473,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const keySettings: TableWidgetDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(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.decimals = dataKey.decimals;
header.push({
@ -386,6 +484,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
styleInfo,
contentInfo,
show: true,
columnDefaultVisibility,
columnSelectionAvailability,
order: index + 2
});
});
@ -396,6 +496,8 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
const keySettings: TimeseriesWidgetLatestDataKeySettings = dataKey.settings;
const styleInfo = getCellStyleInfo(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.decimals = dataKey.decimals;
header.push({
@ -405,13 +507,13 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI
styleInfo,
contentInfo,
show: isDefinedAndNotNull(keySettings.show) ? keySettings.show : true,
columnDefaultVisibility,
columnSelectionAvailability,
order: isDefinedAndNotNull(keySettings.order) ? keySettings.order : (index + 2)
});
});
}
header = header.sort((a, b) => {
return a.order - b.order;
});
header = header.sort((a, b) => a.order - b.order);
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 { coerceBoolean } from '@shared/decorators/coercion';
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 Timeout = NodeJS.Timeout;
const emptySettingsSchema: JsonSchema = {
type: 'object',
@ -96,7 +96,7 @@ const defaultSettingsForm = [
@Component({
selector: 'tb-widget-config',
templateUrl: './widget-config.component.html',
styleUrls: ['./widget-config.component.scss', './config/widget-config.scss'],
styleUrls: ['./widget-config.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
@ -143,6 +143,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
@coerceBoolean()
hideToggleHeader = false;
@Input()
@coerceBoolean()
isAdd = false;
@Input() disabled: boolean;
@Input()
@ -399,22 +403,22 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
writeValue(value: WidgetConfigComponentData): void {
this.modelValue = value;
this.widgetType = this.modelValue?.widgetType;
this.setupConfig();
this.setupConfig(this.isAdd);
}
private setupConfig() {
private setupConfig(isAdd = false) {
if (this.modelValue) {
this.destroyBasicModeComponent();
this.removeChangeSubscriptions();
if (this.hasBasicModeDirective && this.widgetConfigMode === WidgetConfigMode.basic) {
this.setupBasicModeConfig();
this.setupBasicModeConfig(isAdd);
} else {
this.setupDefaultConfig();
}
}
}
private setupBasicModeConfig() {
private setupBasicModeConfig(isAdd = false) {
const componentType = basicWidgetConfigComponentsMap[this.modelValue.basicModeDirective];
if (!componentType) {
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.basicModeComponentRef = this.basicModeContainer.createComponent(factory);
this.basicModeComponent = this.basicModeComponentRef.instance;
this.basicModeComponent.isAdd = isAdd;
this.basicModeComponent.widgetConfig = this.modelValue;
this.basicModeComponentChangeSubscription = this.basicModeComponent.widgetConfigChanged.subscribe((data) => {
this.modelValue = data;
@ -826,7 +831,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
const entityFilter = singleEntityFilterFromDeviceId(deviceId);
return this.entityService.getEntityKeysByEntityFilter(
entityFilter,
dataKeyTypes,
dataKeyTypes, [EntityType.DEVICE],
{ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))
@ -837,7 +842,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
return this.aliasController.getAliasInfo(entityAliasId).pipe(
mergeMap((aliasInfo) => this.entityService.getEntityKeysByEntityFilter(
aliasInfo.entityFilter,
dataKeyTypes,
dataKeyTypes, [],
{ignoreLoading: true, ignoreErrors: true}
).pipe(
catchError(() => of([]))

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

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

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

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

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

@ -15,55 +15,48 @@
limitations under the License.
-->
<div class="mat-content" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
[cheatSheet]="cheatSheetComponent"
fxLayout="column" class="tb-rulechain">
<div class="mat-content tb-rulechain" fxFlex tb-fullscreen [fullscreen]="isFullscreen" tb-hotkeys [hotkeys]="hotKeys"
[cheatSheet]="cheatSheetComponent" fxLayout="column">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-rulechain-container" fxFlex fxLayout="column">
<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 class="tb-rulechain-library mat-elevation-z4"
disableClose="true"
mode="side"
[opened]="isLibraryOpen"
#drawer
opened
position="start"
fxLayout="column">
<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">
<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">
<button mat-icon-button matPrefix class="tb-small"
matTooltip="{{'rulenode.search' | translate}}"
matTooltipPosition="above">
<mat-icon>search</mat-icon>
</button>
<input #ruleNodeSearchInput matInput
[(ngModel)]="ruleNodeTypeSearch"
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>
<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>
</mat-toolbar>
<div class="tb-rulechain-library-panel-group">
@ -164,6 +157,14 @@
</tb-details-panel>
</mat-drawer>
<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
*ngIf="!isImport"
color="primary"

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

@ -54,20 +54,43 @@
z-index: 2;
}
section.tb-header-buttons.tb-library-open {
.tb-library-node-btn {
width: 20px;
height: 90px;
position: absolute;
top: 0;
left: 0;
z-index: 2;
pointer-events: none;
.mdc-fab.tb-btn-open-library {
top: 0;
top: 50%;
z-index: 1000;
opacity: 0.5;
border-radius: 0 15px 15px 0 !important;
transform: translateY(-50%);
&-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;
width: 36px;
height: 36px;
margin: 4px 0 0 4px;
line-height: 36px;
opacity: .5;
content: "";
pointer-events: none;
border-top-left-radius: 15px;
box-shadow: -10px 0px 0px 0px #305680;
border: none;
}
}
@ -77,8 +100,7 @@
min-width: 250px;
.mat-toolbar {
height: 48px;
min-height: 48px;
height: min-content;
padding: 0;
.mat-toolbar-tools {

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

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

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

@ -29,6 +29,14 @@
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</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
mat-button color="primary" matSuffix
*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 { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms';
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 { AppState } from '@core/core.state';
import { TranslateService } from '@ngx-translate/core';
@ -66,6 +66,10 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
@coerceBoolean()
allowCreate = false;
@Input()
@coerceBoolean()
allowEdit = false;
@Input()
disabled: boolean;
@ -196,18 +200,34 @@ export class TemplateAutocompleteComponent implements ControlValueAccessor, OnIn
}, 0);
}
editTemplate($event: Event) {
if ($event) {
$event.stopPropagation();
}
this.notificationService.getNotificationTemplateById(this.modelValue.id).subscribe(
(template) => {
this.openNotificationTemplateDialog({template});
}
);
}
createTemplate($event: Event, button: MatButton) {
if ($event) {
$event.stopPropagation();
}
button._elementRef.nativeElement.blur();
this.openNotificationTemplateDialog({
isAdd: true,
predefinedType: this.notificationTypes
});
}
private openNotificationTemplateDialog(dialogData?: TemplateNotificationDialogData) {
this.dialog.open<TemplateNotificationDialogComponent, TemplateNotificationDialogData,
NotificationTemplate>(TemplateNotificationDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
predefinedType: this.notificationTypes
}
data: dialogData
}).afterClosed()
.subscribe((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 { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
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;
@ -373,7 +374,8 @@ export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ToggleHeaderComponent
ToggleHeaderComponent,
RuleChainSelectComponent
],
imports: [
CommonModule,
@ -606,7 +608,8 @@ export const MarkedOptionsFactory = (markedOptionsService: MarkedOptionsService)
GtMdLgLayoutGapDirective,
GtMdLgShowHideDirective,
ColorPickerComponent,
ToggleHeaderComponent
ToggleHeaderComponent,
RuleChainSelectComponent
]
})
export class SharedModule { }

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

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

162
ui-ngx/src/styles.scss

@ -1183,4 +1183,166 @@ mat-label {
.mat-expansion-panel {
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