Browse Source

Merge branch 'master' into master

pull/13878/head
沉淀 1 year ago
committed by GitHub
parent
commit
8dda6085ac
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      application/src/main/data/json/system/widget_types/rpc_debug_terminal.json
  2. 2
      msa/js-executor/docker/Dockerfile
  3. 22
      msa/js-executor/package.json
  4. 2
      msa/js-executor/pom.xml
  5. 1307
      msa/js-executor/yarn.lock
  6. 2
      msa/web-ui/docker/Dockerfile
  7. 28
      msa/web-ui/package.json
  8. 2
      msa/web-ui/pom.xml
  9. 1329
      msa/web-ui/yarn.lock
  10. 9
      pom.xml
  11. 2
      ui-ngx/pom.xml
  12. 39
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  13. 47
      ui-ngx/src/app/core/utils.ts
  14. 5
      ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts
  15. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  16. 2
      ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html
  17. 12
      ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts
  18. 17
      ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts
  19. 8
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html
  20. 3
      ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts
  21. 3
      ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html
  22. 3
      ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts
  23. 180
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html
  24. 142
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts
  25. 10
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts
  26. 3
      ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html
  27. 21
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  28. 31
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  29. 4
      ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts
  30. 4
      ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html
  31. 5
      ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts
  32. 5
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html
  33. 6
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  34. 172
      ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts
  35. 110
      ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts
  36. 3
      ui-ngx/src/app/shared/components/time/timewindow.component.ts
  37. 5
      ui-ngx/src/app/shared/models/alarm.models.ts
  38. 140
      ui-ngx/src/app/shared/models/time/time.models.ts
  39. 39
      ui-ngx/src/app/shared/models/widget.models.ts
  40. 67
      ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts
  41. 1
      ui-ngx/src/app/shared/models/widget/widget-model.definition.ts
  42. 171
      ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md
  43. 18
      ui-ngx/src/assets/locale/locale.constant-en_US.json

4
application/src/main/data/json/system/widget_types/rpc_debug_terminal.json

@ -11,7 +11,7 @@
"resources": [], "resources": [],
"templateHtml": "<div style=\"height: 100%; overflow-y: auto;\" id=\"device-terminal\"></div>", "templateHtml": "<div style=\"height: 100%; overflow-y: auto;\" id=\"device-terminal\"></div>",
"templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n",
"controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' <method> [params body]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}",
"settingsSchema": "", "settingsSchema": "",
"dataKeySettingsSchema": "{}\n", "dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-rpc-terminal-widget-settings", "settingsDirective": "tb-rpc-terminal-widget-settings",
@ -43,4 +43,4 @@
"public": true "public": true
} }
] ]
} }

2
msa/js-executor/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License. # limitations under the License.
# #
FROM thingsboard/node:20.18.0-bookworm-slim FROM thingsboard/node:22.18.0-bookworm-slim
ENV NODE_ENV production ENV NODE_ENV production
ENV DOCKER_MODE true ENV DOCKER_MODE true

22
msa/js-executor/package.json

@ -6,20 +6,20 @@
"main": "server.ts", "main": "server.ts",
"bin": "server.js", "bin": "server.js",
"scripts": { "scripts": {
"pkg": "tsc && pkg -t node18-linux-x64,node18-win-x64 --out-path ./target ./target/src && node install.js", "pkg": "tsc && pkg -t node22-linux-x64,node22-win-x64 --out-path ./target ./target/src && node install.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon --watch '.' --ext 'ts' --exec 'ts-node server.ts'", "start": "nodemon --watch '.' --ext 'ts' --exec 'ts-node server.ts'",
"start-prod": "nodemon --watch '.' --ext 'ts' --exec 'NODE_ENV=production ts-node server.ts'", "start-prod": "nodemon --watch '.' --ext 'ts' --exec 'NODE_ENV=production ts-node server.ts'",
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"config": "^3.3.12", "config": "^4.1.1",
"express": "^4.21.1", "express": "^5.1.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"kafkajs": "^2.2.4", "kafkajs": "^2.2.4",
"long": "^5.2.3", "long": "^5.3.2",
"uuid-parse": "^1.1.0", "uuid-parse": "^1.1.0",
"winston": "^3.16.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
}, },
"nyc": { "nyc": {
@ -32,14 +32,14 @@
}, },
"devDependencies": { "devDependencies": {
"@types/config": "^3.3.5", "@types/config": "^3.3.5",
"@types/express": "~4.17.21", "@types/express": "~5.0.3",
"@types/node": "~20.17.6", "@types/node": "~22.17.2",
"@types/uuid-parse": "^1.0.2", "@types/uuid-parse": "^1.0.2",
"fs-extra": "^11.2.0", "@yao-pkg/pkg": "^6.6.0",
"nodemon": "^3.1.7", "fs-extra": "^11.3.1",
"pkg": "^5.8.1", "nodemon": "^3.1.10",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "5.5.4" "typescript": "5.9.2"
}, },
"pkg": { "pkg": {
"assets": [ "assets": [

2
msa/js-executor/pom.xml

@ -71,7 +71,7 @@
<goal>install-node-and-yarn</goal> <goal>install-node-and-yarn</goal>
</goals> </goals>
<configuration> <configuration>
<nodeVersion>v20.18.0</nodeVersion> <nodeVersion>v22.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion> <yarnVersion>v1.22.22</yarnVersion>
</configuration> </configuration>
</execution> </execution>

1307
msa/js-executor/yarn.lock

File diff suppressed because it is too large

2
msa/web-ui/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License. # limitations under the License.
# #
FROM thingsboard/node:20.18.0-bookworm-slim FROM thingsboard/node:22.18.0-bookworm-slim
ENV NODE_ENV production ENV NODE_ENV production
ENV DOCKER_MODE true ENV DOCKER_MODE true

28
msa/web-ui/package.json

@ -6,21 +6,21 @@
"main": "server.ts", "main": "server.ts",
"bin": "server.js", "bin": "server.js",
"scripts": { "scripts": {
"pkg": "tsc && pkg -t node18-linux-x64,node18-win-x64 --out-path ./target ./target/src && node install.js", "pkg": "tsc && pkg -t node22-linux-x64,node22-win-x64 --out-path ./target ./target/src && node install.js",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web ts-node server.ts'", "start": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web ts-node server.ts'",
"start-prod": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web NODE_ENV=production ts-node server.ts'", "start-prod": "nodemon --watch '.' --ext 'ts' --exec 'WEB_FOLDER=./target/web NODE_ENV=production ts-node server.ts'",
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"compression": "^1.7.5", "compression": "^1.8.1",
"config": "^3.3.12", "config": "^3.3.12",
"connect-history-api-fallback": "^1.6.0", "connect-history-api-fallback": "1.6.0",
"express": "^4.21.1", "express": "^5.1.0",
"http": "0.0.0", "http": "0.0.0",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"winston": "^3.16.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
}, },
"nyc": { "nyc": {
@ -32,17 +32,17 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@types/compression": "^1.7.5", "@types/compression": "^1.8.1",
"@types/config": "^3.3.5", "@types/config": "^3.3.5",
"@types/connect-history-api-fallback": "^1.5.4", "@types/connect-history-api-fallback": "1.5.4",
"@types/express": "~4.17.21", "@types/express": "~5.0.3",
"@types/http-proxy": "^1.17.15", "@types/http-proxy": "^1.17.16",
"@types/node": "~20.17.6", "@types/node": "~22.17.2",
"fs-extra": "^11.2.0", "@yao-pkg/pkg": "^6.6.0",
"nodemon": "^3.1.7", "fs-extra": "^11.3.1",
"pkg": "^5.8.1", "nodemon": "^3.1.10",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "5.5.4" "typescript": "5.9.2"
}, },
"pkg": { "pkg": {
"assets": [ "assets": [

2
msa/web-ui/pom.xml

@ -80,7 +80,7 @@
<goal>install-node-and-yarn</goal> <goal>install-node-and-yarn</goal>
</goals> </goals>
<configuration> <configuration>
<nodeVersion>v20.18.0</nodeVersion> <nodeVersion>v22.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion> <yarnVersion>v1.22.22</yarnVersion>
</configuration> </configuration>
</execution> </execution>

1329
msa/web-ui/yarn.lock

File diff suppressed because it is too large

9
pom.xml

@ -146,6 +146,7 @@
<firebase-admin.version>9.2.0</firebase-admin.version> <firebase-admin.version>9.2.0</firebase-admin.version>
<snappy.version>1.1.10.5</snappy.version> <snappy.version>1.1.10.5</snappy.version>
<rocksdbjni.version>9.10.0</rocksdbjni.version> <rocksdbjni.version>9.10.0</rocksdbjni.version>
<netty.version>4.1.124.Final</netty.version>
</properties> </properties>
<modules> <modules>
@ -895,6 +896,13 @@
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-bom</artifactId>
<version>${netty.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId> <artifactId>spring-boot-dependencies</artifactId>
@ -909,7 +917,6 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>netty-mqtt</artifactId> <artifactId>netty-mqtt</artifactId>

2
ui-ngx/pom.xml

@ -56,7 +56,7 @@
<goal>install-node-and-yarn</goal> <goal>install-node-and-yarn</goal>
</goals> </goals>
<configuration> <configuration>
<nodeVersion>v20.18.0</nodeVersion> <nodeVersion>v22.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion> <yarnVersion>v1.22.22</yarnVersion>
</configuration> </configuration>
</execution> </execution>

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

@ -38,6 +38,7 @@ import {
import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils'; import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils';
import { import {
Datasource, Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation, datasourcesHasOnlyComparisonAggregation,
DatasourceType, DatasourceType,
defaultLegendConfig, defaultLegendConfig,
@ -49,7 +50,8 @@ import {
WidgetConfigMode, WidgetConfigMode,
WidgetSize, WidgetSize,
widgetType, widgetType,
WidgetTypeDescriptor WidgetTypeDescriptor,
widgetTypeHasTimewindow
} from '@app/shared/models/widget.models'; } from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
@ -234,6 +236,7 @@ export class DashboardUtilsService {
public validateAndUpdateWidget(widget: Widget): Widget { public validateAndUpdateWidget(widget: Widget): Widget {
widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type); widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type);
widget = this.validateAndUpdateWidgetTypeFqn(widget); widget = this.validateAndUpdateWidgetTypeFqn(widget);
this.removeTimewindowConfigIfUnused(widget);
if (isDefined((widget as any).title)) { if (isDefined((widget as any).title)) {
delete (widget as any).title; delete (widget as any).title;
} }
@ -294,8 +297,11 @@ export class DashboardUtilsService {
} }
widgetConfig.datasources = this.validateAndUpdateDatasources(widgetConfig.datasources); widgetConfig.datasources = this.validateAndUpdateDatasources(widgetConfig.datasources);
if (type === widgetType.latest) { if (type === widgetType.latest) {
const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); if (datasourcesHasAggregation(widgetConfig.datasources)) {
widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService); const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources);
widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true,
onlyHistoryTimewindow, this.timeService, false);
}
} else if (type === widgetType.rpc) { } else if (type === widgetType.rpc) {
if (widgetConfig.targetDeviceAliasIds && widgetConfig.targetDeviceAliasIds.length) { if (widgetConfig.targetDeviceAliasIds && widgetConfig.targetDeviceAliasIds.length) {
widgetConfig.targetDevice = { widgetConfig.targetDevice = {
@ -348,6 +354,33 @@ export class DashboardUtilsService {
return widgetConfig; return widgetConfig;
} }
private removeTimewindowConfigIfUnused(widget: Widget) {
const widgetHasTimewindow = this.widgetHasTimewindow(widget);
if (!widgetHasTimewindow || widget.config.useDashboardTimewindow) {
delete widget.config.displayTimewindow;
delete widget.config.timewindow;
delete widget.config.timewindowStyle;
if (!widgetHasTimewindow) {
delete widget.config.useDashboardTimewindow;
}
}
}
private widgetHasTimewindow(widget: Widget): boolean {
const widgetDefinition = findWidgetModelDefinition(widget);
if (widgetDefinition) {
return widgetDefinition.hasTimewindow(widget);
}
return widgetTypeHasTimewindow(widget.type)
|| (widget.type === widgetType.latest && datasourcesHasAggregation(widget.config.datasources));
}
public prepareWidgetForSaving(widget: Widget): Widget {
this.removeTimewindowConfigIfUnused(widget);
return widget;
}
public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget { public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget {
const config = widget.config; const config = widget.config;
config.showTitle = false; config.showTitle = false;

47
ui-ngx/src/app/core/utils.ts

@ -27,7 +27,8 @@ import { serverErrorCodesTranslations } from '@shared/models/constants';
import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models';
import { import {
CompiledTbFunction, CompiledTbFunction,
compileTbFunction, GenericFunction, compileTbFunction,
GenericFunction,
isNotEmptyTbFunction, isNotEmptyTbFunction,
TbFunction TbFunction
} from '@shared/models/js-function.models'; } from '@shared/models/js-function.models';
@ -196,6 +197,23 @@ export function deleteNullProperties(obj: any) {
}); });
} }
export function deleteFalseProperties(obj: Record<string, any>): void {
if (isUndefinedOrNull(obj)) {
return;
}
Object.keys(obj).forEach((propName) => {
if (obj[propName] === false || isUndefinedOrNull(obj[propName])) {
delete obj[propName];
} else if (isObject(obj[propName])) {
deleteFalseProperties(obj[propName]);
} else if (Array.isArray(obj[propName])) {
(obj[propName] as any[]).forEach((elem) => {
deleteFalseProperties(elem);
});
}
});
}
export function objToBase64(obj: any): string { export function objToBase64(obj: any): string {
const json = JSON.stringify(obj); const json = JSON.stringify(obj);
return btoa(encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, return btoa(encodeURIComponent(json).replace(/%([0-9A-F]{2})/g,
@ -773,6 +791,33 @@ export function deepTrim<T>(obj: T): T {
}, (Array.isArray(obj) ? [] : {}) as T); }, (Array.isArray(obj) ? [] : {}) as T);
} }
export function deepClean<T extends Record<string, any> | any[]>(obj: T, {
cleanKeys = []
} = {}): T {
return _.transform(obj, (result, value, key) => {
if (cleanKeys.includes(key)) {
return;
}
if (Array.isArray(value) || isLiteralObject(value)) {
value = deepClean(value, {cleanKeys});
}
if(isLiteralObject(value) && isEmpty(value)) {
return;
}
if (Array.isArray(value) && !value.length) {
return;
}
if (value === undefined || value === null || value === '' || Number.isNaN(value)) {
return;
}
if (Array.isArray(result)) {
return result.push(value);
}
result[key] = value;
});
}
export function generateSecret(length?: number): string { export function generateSecret(length?: number): string {
if (isUndefined(length) || length == null) { if (isUndefined(length) || length == null) {
length = 1; length = 1;

5
ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts

@ -41,6 +41,7 @@ import { deepTrim } from '@core/utils';
export interface AIModelDialogData { export interface AIModelDialogData {
AIModel?: AiModel; AIModel?: AiModel;
isAdd?: boolean; isAdd?: boolean;
name?: string;
} }
@Component({ @Component({
@ -111,6 +112,10 @@ export class AIModelDialogComponent extends DialogComponent<AIModelDialogCompone
}) })
}); });
if (this.data.name) {
this.aiModelForms.get('name').patchValue(this.data.name, {emitEvent: false});
}
this.aiModelForms.get('configuration.provider').valueChanges.pipe( this.aiModelForms.get('configuration.provider').valueChanges.pipe(
takeUntilDestroyed() takeUntilDestroyed()
).subscribe((provider: AiProvider) => { ).subscribe((provider: AiProvider) => {

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

@ -1323,6 +1323,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
} }
private addWidgetToDashboard(widget: Widget) { private addWidgetToDashboard(widget: Widget) {
this.dashboardUtils.prepareWidgetForSaving(widget);
if (this.addingLayoutCtx) { if (this.addingLayoutCtx) {
this.addWidgetToLayout(widget, this.addingLayoutCtx.id); this.addWidgetToLayout(widget, this.addingLayoutCtx.id);
this.addingLayoutCtx = null; this.addingLayoutCtx = null;
@ -1410,7 +1411,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
saveWidget() { saveWidget() {
this.editWidgetComponent.widgetFormGroup.markAsPristine(); this.editWidgetComponent.widgetFormGroup.markAsPristine();
const widget = deepClone(this.editingWidget); const widget = this.dashboardUtils.prepareWidgetForSaving(deepClone(this.editingWidget));
const widgetLayout = deepClone(this.editingWidgetLayout); const widgetLayout = deepClone(this.editingWidgetLayout);
const id = this.editingWidgetOriginal.id; const id = this.editingWidgetOriginal.id;
this.dashboardConfiguration.widgets[id] = widget; this.dashboardConfiguration.widgets[id] = widget;

2
ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html

@ -29,7 +29,7 @@
labelText="rule-node-config.ai.model" labelText="rule-node-config.ai.model"
(entityChanged)="onEntityChange($event)" (entityChanged)="onEntityChange($event)"
[entityType]="entityType.AI_MODEL" [entityType]="entityType.AI_MODEL"
(createNew)="createModelAi('modelId')" (createNew)="createModelAi($event, 'modelId')"
formControlName="modelId"> formControlName="modelId">
</tb-entity-autocomplete> </tb-entity-autocomplete>
</section> </section>

12
ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts

@ -74,11 +74,12 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
} }
} }
protected prepareOutputConfig(configuration: RuleNodeConfiguration): RuleNodeConfiguration { protected prepareOutputConfig(): RuleNodeConfiguration {
const config = this.configForm().getRawValue();
if (!this.aiConfigForm.get('systemPrompt').value) { if (!this.aiConfigForm.get('systemPrompt').value) {
delete configuration.systemPrompt; delete config.systemPrompt;
} }
return deepTrim(configuration); return deepTrim(config);
} }
onEntityChange($event: AiModel) { onEntityChange($event: AiModel) {
@ -98,12 +99,13 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent {
return this.translate.instant(`rule-node-config.ai.response-format-hint-${this.aiConfigForm.get('responseFormat.type').value}`); return this.translate.instant(`rule-node-config.ai.response-format-hint-${this.aiConfigForm.get('responseFormat.type').value}`);
} }
createModelAi(formControl: string) { createModelAi(name: string, formControl: string) {
this.dialog.open<AIModelDialogComponent, AIModelDialogData, AiModel>(AIModelDialogComponent, { this.dialog.open<AIModelDialogComponent, AIModelDialogData, AiModel>(AIModelDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
isAdd: true isAdd: true,
name
} }
}).afterClosed() }).afterClosed()
.subscribe((model) => { .subscribe((model) => {

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

@ -23,11 +23,19 @@ import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { DataKey, DatasourceType, Widget, WidgetConfigMode, widgetType } from '@shared/models/widget.models'; import {
DataKey,
DatasourceType,
Widget,
widgetTypeCanHaveTimewindow,
WidgetConfigMode,
widgetType
} from '@shared/models/widget.models';
import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component';
import { isDefinedAndNotNull } from '@core/utils'; import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks;
@ -107,6 +115,11 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
if (this.isAdd) { if (this.isAdd) {
this.setupDefaults(widgetConfig); this.setupDefaults(widgetConfig);
} }
if (widgetTypeCanHaveTimewindow(widgetConfig.widgetType) && isUndefinedOrNull(widgetConfig.config.timewindow)) {
widgetConfig.config.timewindow = initModelFromDefaultTimewindow(null,
widgetConfig.widgetType === widgetType.latest, false, this.widgetConfigComponent.timeService,
widgetConfig.widgetType === widgetType.timeseries);
}
this.onConfigSet(widgetConfig); this.onConfigSet(widgetConfig);
this.updateValidators(false); this.updateValidators(false);
for (const trigger of this.validatorTriggers()) { for (const trigger of this.validatorTriggers()) {

8
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html

@ -45,14 +45,14 @@
</span> </span>
<span class="flex-1"></span> <span class="flex-1"></span>
<button *ngIf="allowAcknowledgment" <button *ngIf="allowAcknowledgment"
mat-icon-button [disabled]="isLoading$ | async" mat-icon-button
matTooltip="{{ 'alarm.acknowledge' | translate }}" matTooltip="{{ 'alarm.acknowledge' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="ackAlarms($event)"> (click)="ackAlarms($event)">
<mat-icon>done</mat-icon> <mat-icon>done</mat-icon>
</button> </button>
<button *ngIf="ctx.settings.allowClear" mat-icon-button <button *ngIf="ctx.settings.allowClear"
[disabled]="isLoading$ | async" mat-icon-button
matTooltip="{{ 'alarm.clear' | translate }}" matTooltip="{{ 'alarm.clear' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="clearAlarms($event)"> (click)="clearAlarms($event)">
@ -107,7 +107,7 @@
</span> </span>
</ng-template> </ng-template>
<button *ngIf="allowAssign" <button *ngIf="allowAssign"
mat-icon-button [disabled]="isLoading$ | async" mat-icon-button
matTooltip="{{ 'alarm.assign' | translate }}" matTooltip="{{ 'alarm.assign' | translate }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="openAlarmAssigneePanel($event, alarm)"> (click)="openAlarmAssigneePanel($event, alarm)">

3
ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts

@ -851,6 +851,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
content = this.defaultContent(key, contentInfo, value); content = this.defaultContent(key, contentInfo, value);
} }
if (isDefined(content)) { if (isDefined(content)) {
if (typeof content === 'object') {
content = JSON.stringify(content);
}
content = this.utils.customTranslation(content, content); content = this.utils.customTranslation(content, content);
switch (typeof content) { switch (typeof content) {
case 'string': case 'string':

3
ui-ngx/src/app/modules/home/components/widget/lib/entity/entities-table-widget.component.html

@ -65,7 +65,7 @@
[style.min-width]="(entityDatasource.countCellButtonAction * 48) + 'px'"> [style.min-width]="(entityDatasource.countCellButtonAction * 48) + 'px'">
<ng-container *ngFor="let actionDescriptor of entity.actionCellButtons; trackBy: trackByActionCellDescriptionId"> <ng-container *ngFor="let actionDescriptor of entity.actionCellButtons; trackBy: trackByActionCellDescriptionId">
<span *ngIf="!actionDescriptor.icon" style="width: 48px;"></span> <span *ngIf="!actionDescriptor.icon" style="width: 48px;"></span>
<button mat-icon-button [disabled]="isLoading$ | async" <button mat-icon-button
*ngIf="actionDescriptor.icon" *ngIf="actionDescriptor.icon"
matTooltip="{{ actionDescriptor.displayName }}" matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above" matTooltipPosition="above"
@ -83,7 +83,6 @@
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> <mat-menu #cellActionsMenu="matMenu" xPosition="before">
<ng-container *ngFor="let actionDescriptor of entity.actionCellButtons; trackBy: trackByActionCellDescriptionId"> <ng-container *ngFor="let actionDescriptor of entity.actionCellButtons; trackBy: trackByActionCellDescriptionId">
<button mat-menu-item *ngIf="actionDescriptor.icon" <button mat-menu-item *ngIf="actionDescriptor.icon"
[disabled]="isLoading$ | async"
(click)="onActionButtonClick($event, entity, actionDescriptor)"> (click)="onActionButtonClick($event, entity, actionDescriptor)">
<tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon> <tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
<span>{{ actionDescriptor.displayName }}</span> <span>{{ actionDescriptor.displayName }}</span>

3
ui-ngx/src/app/modules/home/components/widget/lib/maps/data-layer/data-layer-utils.ts

@ -56,6 +56,9 @@ export const createTooltip = (map: TbMap<any>,
} }
} }
}); });
layer.on('mousemove', (e) => {
tooltip.setLatLng(e.latlng);
});
layer.on('mouseout', () => { layer.on('mouseout', () => {
tooltip.close(); tooltip.close();
}); });

180
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.html

@ -35,117 +35,73 @@
</mat-icon> </mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
<ng-container [formGroup]="mobileActionTypeFormGroup" [ngSwitch]="mobileActionFormGroup.get('type').value"> <ng-container [formGroup]="mobileActionTypeFormGroup">
<ng-template [ngSwitchCase]="mobileActionType.deviceProvision"> @if (mobileActionFormGroup.get('type').value === mobileActionType.takePhoto ||
<tb-js-func mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery ||
formControlName="handleProvisionSuccessFunction" mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot) {
functionName="handleProvisionSuccess" <div class="tb-form-row">
withModules <mat-slide-toggle class="mat-slide" formControlName="saveToGallery">
[functionArgs]="['deviceName', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" {{ 'widget-action.mobile.save-to-gallery' | translate }}
[globalVariables]="functionScopeVariables" </mat-slide-toggle>
[editorCompleter]="customActionEditorCompleter" </div>
hideBrackets }
></tb-js-func> @if (mobileActionFormGroup.get('type').value === mobileActionType.deviceProvision) {
</ng-template> <div class="tb-form-row">
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection || <div class="fixed-title-width">{{ 'widget-action.mobile.provision-type' | translate }}*</div>
mobileActionFormGroup.get('type').value === mobileActionType.mapLocation ? <mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
mobileActionFormGroup.get('type').value : ''"> <mat-select formControlName="provisionType">
<tb-js-func <mat-option *ngFor="let type of provisionTypes" [value]="type">
formControlName="getLocationFunction" {{ provisionTypeTranslationMap.get(type) | translate }}
functionName="getLocation" </mat-option>
withModules </mat-select>
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" </mat-form-field>
[globalVariables]="functionScopeVariables" </div>
[editorCompleter]="customActionEditorCompleter" }
hideBrackets
helpId="widget/action/mobile_get_location_fn" @for (config of actionConfig; track config.formControlName) {
></tb-js-func> <div class="tb-form-panel stroked">
</ng-template> <mat-expansion-panel class="tb-settings">
<ng-template [ngSwitchCase]="mobileActionType.makePhoneCall"> <mat-expansion-panel-header>
<tb-js-func <mat-panel-description class="flex items-stretch justify-start">
formControlName="getPhoneNumberFunction" {{ config.title | translate }}
functionName="getPhoneNumber" </mat-panel-description>
withModules </mat-expansion-panel-header>
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" <tb-js-func
[globalVariables]="functionScopeVariables" [formControlName]="config.formControlName"
[editorCompleter]="customActionEditorCompleter" [functionName]="config.functionName"
hideBrackets withModules
helpId="widget/action/mobile_get_phone_number_fn" [functionArgs]="config.functionArgs"
></tb-js-func> [globalVariables]="functionScopeVariables"
</ng-template> [editorCompleter]="customActionEditorCompleter"
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.takePhoto || hideBrackets
mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery || [helpId]="config.helpId"
mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot ? ></tb-js-func>
mobileActionFormGroup.get('type').value : ''"> </mat-expansion-panel>
<tb-js-func </div>
formControlName="processImageFunction" }
functionName="processImage"
withModules
[functionArgs]="['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_process_image_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.scanQrCode">
<tb-js-func
formControlName="processQrCodeFunction"
functionName="processQrCode"
withModules
[functionArgs]="['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_process_qr_code_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.getLocation">
<tb-js-func
formControlName="processLocationFunction"
functionName="processLocation"
withModules
[functionArgs]="['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_process_location_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection ||
mobileActionFormGroup.get('type').value === mobileActionType.mapLocation ||
mobileActionFormGroup.get('type').value === mobileActionType.makePhoneCall ?
mobileActionFormGroup.get('type').value : ''">
<tb-js-func
formControlName="processLaunchResultFunction"
functionName="processLaunchResult"
withModules
[functionArgs]="['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_process_launch_result_fn"
></tb-js-func>
</ng-template>
</ng-container> </ng-container>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleEmptyResultFunction" @if(mobileActionFormGroup.get('type').value) {
functionName="handleEmptyResult" @for (config of commonActionConfig; track config.formControlName) {
withModules <div class="tb-form-panel stroked">
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" <mat-expansion-panel class="tb-settings">
[globalVariables]="functionScopeVariables" <mat-expansion-panel-header>
[editorCompleter]="customActionEditorCompleter" <mat-panel-description class="flex items-stretch justify-start">
hideBrackets {{ config.title | translate }}
helpId="widget/action/mobile_handle_empty_result_fn" </mat-panel-description>
></tb-js-func> </mat-expansion-panel-header>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value" <tb-js-func
formControlName="handleErrorFunction" [formControlName]="config.formControlName"
functionName="handleError" [functionName]="config.functionName"
withModules withModules
[functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']" [functionArgs]="config.functionArgs"
[globalVariables]="functionScopeVariables" [globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter" [editorCompleter]="customActionEditorCompleter"
hideBrackets hideBrackets
helpId="widget/action/mobile_handle_error_fn" [helpId]="config.helpId"
></tb-js-func> ></tb-js-func>
</mat-expansion-panel>
</div>
}
}
</div> </div>

142
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.component.ts

@ -24,6 +24,9 @@ import {
} from '@angular/forms'; } from '@angular/forms';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { import {
ActionConfig,
ProvisionType,
provisionTypeTranslationMap,
WidgetActionType, WidgetActionType,
WidgetMobileActionDescriptor, WidgetMobileActionDescriptor,
WidgetMobileActionType, WidgetMobileActionType,
@ -35,6 +38,7 @@ import {
getDefaultGetPhoneNumberFunction, getDefaultGetPhoneNumberFunction,
getDefaultHandleEmptyResultFunction, getDefaultHandleEmptyResultFunction,
getDefaultHandleErrorFunction, getDefaultHandleErrorFunction,
getDefaultHandleNonMobileFallBackFunction,
getDefaultProcessImageFunction, getDefaultProcessImageFunction,
getDefaultProcessLaunchResultFunction, getDefaultProcessLaunchResultFunction,
getDefaultProcessLocationFunction, getDefaultProcessLocationFunction,
@ -68,6 +72,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
functionScopeVariables: string[]; functionScopeVariables: string[];
actionConfig: ActionConfig[];
commonActionConfig: ActionConfig[];
provisionTypes: string[] = Object.keys(ProvisionType);
provisionTypeTranslationMap = provisionTypeTranslationMap;
private requiredValue: boolean; private requiredValue: boolean;
get required(): boolean { get required(): boolean {
return this.requiredValue; return this.requiredValue;
@ -99,8 +109,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
this.mobileActionFormGroup = this.fb.group({ this.mobileActionFormGroup = this.fb.group({
type: [null, Validators.required], type: [null, Validators.required],
handleEmptyResultFunction: [null], handleEmptyResultFunction: [null],
handleErrorFunction: [null] handleErrorFunction: [null],
handleNonMobileFallbackFunction: [null]
}); });
this.getCommonActionConfigs();
this.mobileActionFormGroup.get('type').valueChanges.pipe( this.mobileActionFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe((type: WidgetMobileActionType) => { ).subscribe((type: WidgetMobileActionType) => {
@ -109,6 +121,7 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
action = {...action, ...this.mobileActionTypeFormGroup.value}; action = {...action, ...this.mobileActionTypeFormGroup.value};
} }
this.updateMobileActionType(type, action); this.updateMobileActionType(type, action);
this.getActionConfigs();
}); });
this.mobileActionFormGroup.valueChanges.pipe( this.mobileActionFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
@ -133,10 +146,14 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
} }
writeValue(value: WidgetMobileActionDescriptor | null): void { writeValue(value: WidgetMobileActionDescriptor | null): void {
this.mobileActionFormGroup.patchValue({type: value?.type, this.mobileActionFormGroup.patchValue({
handleEmptyResultFunction: value?.handleEmptyResultFunction, type: value?.type,
handleErrorFunction: value?.handleErrorFunction}, {emitEvent: false}); handleEmptyResultFunction: value?.handleEmptyResultFunction,
handleErrorFunction: value?.handleErrorFunction,
handleNonMobileFallbackFunction: value?.handleNonMobileFallbackFunction
}, {emitEvent: false});
this.updateMobileActionType(value?.type, value); this.updateMobileActionType(value?.type, value);
this.getActionConfigs();
} }
private updateModel() { private updateModel() {
@ -164,6 +181,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
handleErrorFunction = getDefaultHandleErrorFunction(type); handleErrorFunction = getDefaultHandleErrorFunction(type);
this.mobileActionFormGroup.patchValue({handleErrorFunction}, {emitEvent: false}); this.mobileActionFormGroup.patchValue({handleErrorFunction}, {emitEvent: false});
} }
let handleNonMobileFallbackFunction = action?.handleNonMobileFallbackFunction;
const defaultHandleNonMobileFallbackFunction = getDefaultHandleNonMobileFallBackFunction();
if (defaultHandleNonMobileFallbackFunction !== handleNonMobileFallbackFunction) {
handleNonMobileFallbackFunction = getDefaultHandleNonMobileFallBackFunction();
this.mobileActionFormGroup.patchValue({handleNonMobileFallbackFunction}, {emitEvent: false});
}
} }
this.mobileActionTypeFormGroup = this.fb.group({}); this.mobileActionTypeFormGroup = this.fb.group({});
if (type) { if (type) {
@ -183,6 +206,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
'processImageFunction', 'processImageFunction',
this.fb.control(processImageFunction, []) this.fb.control(processImageFunction, [])
); );
this.mobileActionTypeFormGroup.addControl(
'saveToGallery',
this.fb.control(action?.saveToGallery || false, [])
);
break; break;
case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapDirection:
case WidgetMobileActionType.mapLocation: case WidgetMobileActionType.mapLocation:
@ -267,6 +294,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
'handleProvisionSuccessFunction', 'handleProvisionSuccessFunction',
this.fb.control(handleProvisionSuccessFunction, [Validators.required]) this.fb.control(handleProvisionSuccessFunction, [Validators.required])
); );
this.mobileActionTypeFormGroup.addControl(
'provisionType',
this.fb.control(action?.provisionType || ProvisionType.auto, [])
);
} }
} }
this.mobileActionTypeFormGroup.valueChanges.pipe( this.mobileActionTypeFormGroup.valueChanges.pipe(
@ -276,5 +307,108 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
}); });
} }
getActionConfigs() {
const type = this.mobileActionFormGroup.get('type').value;
this.actionConfig = [];
switch (type) {
case this.mobileActionType.deviceProvision:
this.actionConfig.push({
title: 'widget-action.mobile.handle-provision-success-function',
formControlName: 'handleProvisionSuccessFunction',
functionName: 'handleProvisionSuccess',
functionArgs: ['deviceName', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']
});
break;
case this.mobileActionType.mapDirection:
case this.mobileActionType.mapLocation:
this.actionConfig.push({
title: 'widget-action.mobile.get-location-function',
formControlName: 'getLocationFunction',
functionName: 'getLocation',
functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'],
helpId: 'widget/action/mobile_get_location_fn'
});
this.actionConfig.push({
title: 'widget-action.mobile.process-launch-result-function',
formControlName: 'processLaunchResultFunction',
functionName: 'processLaunchResult',
functionArgs: ['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'],
helpId: 'widget/action/mobile_process_launch_result_fn'
});
break;
case this.mobileActionType.makePhoneCall:
this.actionConfig.push({
title: 'widget-action.mobile.get-phone-number-function',
formControlName: 'getPhoneNumberFunction',
functionName: 'getPhoneNumber',
functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'],
helpId: 'widget/action/mobile_get_phone_number_fn'
});
this.actionConfig.push({
title: 'widget-action.mobile.process-launch-result-function',
formControlName: 'processLaunchResultFunction',
functionName: 'processLaunchResult',
functionArgs: ['launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'],
helpId: 'widget/action/mobile_process_launch_result_fn'
});
break;
case this.mobileActionType.takePhoto:
case this.mobileActionType.takePictureFromGallery:
case this.mobileActionType.takeScreenshot:
this.actionConfig.push({
title: 'widget-action.mobile.process-image-function',
formControlName: 'processImageFunction',
functionName: 'processImage',
functionArgs: ['imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'],
helpId: 'widget/action/mobile_process_image_fn'
});
break;
case this.mobileActionType.scanQrCode:
this.actionConfig.push({
title: 'widget-action.mobile.process-qr-code-function',
formControlName: 'processQrCodeFunction',
functionName: 'processQrCode',
functionArgs: ['code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'],
helpId: 'widget/action/mobile_process_qr_code_fn'
});
break;
case this.mobileActionType.getLocation:
this.actionConfig.push({
title: 'widget-action.mobile.process-location-function',
formControlName: 'processLocationFunction',
functionName: 'processLocation',
functionArgs: ['latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'],
helpId: 'widget/action/mobile_process_location_fn'
});
break;
}
}
getCommonActionConfigs() {
this.commonActionConfig = [
{
title: 'widget-action.mobile.handle-empty-result-function',
formControlName: 'handleEmptyResultFunction',
functionName: 'handleEmptyResult',
functionArgs: ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'],
helpId: 'widget/action/mobile_handle_empty_result_fn'
},
{
title: 'widget-action.mobile.handle-error-function',
formControlName: 'handleErrorFunction',
functionName: 'handleError',
functionArgs: ['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel'],
helpId: 'widget/action/mobile_handle_error_fn'
},
{
title: 'widget-action.mobile.handle-non-mobile-fallback-function',
formControlName: 'handleNonMobileFallbackFunction',
functionName: 'handleNonMobileFallback',
functionArgs: ['$event', 'widgetContext'],
helpId: 'widget/action/mobile_handle_non_mobile_fallback_fn'
}
];
}
protected readonly WidgetActionType = WidgetActionType; protected readonly WidgetActionType = WidgetActionType;
} }

10
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/action/mobile-action-editor.models.ts

@ -172,6 +172,14 @@ const handleErrorFunctionTemplate: TbFunction =
' }, 100);\n' + ' }, 100);\n' +
'}\n'; '}\n';
const handleNonMobileFallbackFunctionTemplate: TbFunction =
'// Optional function body to handle non-mobile fallback \n' +
'showFallbackToast();\n' +
'\n' +
'function showFallbackToast(title, error) {\n' +
' widgetContext.showWarnToast(\'This action is only available in the mobile application.\');\n' +
'}\n';
const getLocationFunctionTemplate: TbFunction = const getLocationFunctionTemplate: TbFunction =
'// Function body that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.\n' + '// Function body that should return location as array of two numbers (latitude, longitude) for further processing by mobile action.\n' +
'// Usually location can be obtained from entity attributes/telemetry. \n\n' + '// Usually location can be obtained from entity attributes/telemetry. \n\n' +
@ -326,3 +334,5 @@ export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): TbF
} }
return handleErrorFunctionTemplate.replace('--TITLE--', title); return handleErrorFunctionTemplate.replace('--TITLE--', title);
}; };
export const getDefaultHandleNonMobileFallBackFunction = () => handleNonMobileFallbackFunctionTemplate;

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

@ -78,7 +78,7 @@
<span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span> <span *ngIf="!actionDescriptor.icon" style="width: 40px;"></span>
<button *ngIf="actionDescriptor.icon" <button *ngIf="actionDescriptor.icon"
class="tb-mat-40" class="tb-mat-40"
mat-icon-button [disabled]="isLoading$ | async" mat-icon-button
matTooltip="{{ actionDescriptor.displayName }}" matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above" matTooltipPosition="above"
(click)="onActionButtonClick($event, row, actionDescriptor)"> (click)="onActionButtonClick($event, row, actionDescriptor)">
@ -96,7 +96,6 @@
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> <mat-menu #cellActionsMenu="matMenu" xPosition="before">
<ng-container *ngFor="let actionDescriptor of row.actionCellButtons; trackBy: trackByActionCellDescriptionId"> <ng-container *ngFor="let actionDescriptor of row.actionCellButtons; trackBy: trackByActionCellDescriptionId">
<button mat-menu-item *ngIf="actionDescriptor.icon" <button mat-menu-item *ngIf="actionDescriptor.icon"
[disabled]="isLoading$ | async"
(click)="onActionButtonClick($event, row, actionDescriptor)"> (click)="onActionButtonClick($event, row, actionDescriptor)">
<tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon> <tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
<span>{{ actionDescriptor.displayName }}</span> <span>{{ actionDescriptor.displayName }}</span>

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

@ -38,6 +38,7 @@ import {
TargetDevice, TargetDevice,
targetDeviceValid, targetDeviceValid,
Widget, Widget,
widgetTypeCanHaveTimewindow,
WidgetConfigMode, WidgetConfigMode,
widgetType widgetType
} from '@shared/models/widget.models'; } from '@shared/models/widget.models';
@ -53,7 +54,7 @@ import {
Validators Validators
} from '@angular/forms'; } from '@angular/forms';
import { WidgetConfigComponentData } from '@home/models/widget-component.models'; import { WidgetConfigComponentData } from '@home/models/widget-component.models';
import { deepClone, genNextLabel, isDefined, isObject } from '@app/core/utils'; import { deepClone, genNextLabel, isDefined, isDefinedAndNotNull, isObject } from '@app/core/utils';
import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models'; import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
import { EntityAlias } from '@shared/models/alias.models'; import { EntityAlias } from '@shared/models/alias.models';
@ -84,6 +85,8 @@ import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/co
import { defaultFormProperties, FormProperty } from '@shared/models/dynamic-form.models'; import { defaultFormProperties, FormProperty } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { WidgetService } from '@core/http/widget.service'; import { WidgetService } from '@core/http/widget.service';
import { TimeService } from '@core/services/time.service';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
@Component({ @Component({
@ -201,6 +204,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private utils: UtilsService, private utils: UtilsService,
private entityService: EntityService, private entityService: EntityService,
public timeService: TimeService,
private dialog: MatDialog, private dialog: MatDialog,
public translate: TranslateService, public translate: TranslateService,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
@ -366,16 +370,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.dataSettings = this.fb.group({}); this.dataSettings = this.fb.group({});
this.targetDeviceSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({});
this.advancedSettings = this.fb.group({}); this.advancedSettings = this.fb.group({});
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { if (widgetTypeCanHaveTimewindow(this.widgetType)) {
this.dataSettings.addControl('timewindowConfig', this.fb.control({ this.dataSettings.addControl('timewindowConfig', this.fb.control({
useDashboardTimewindow: true, useDashboardTimewindow: true,
displayTimewindow: true, displayTimewindow: true,
timewindow: null, timewindow: null,
timewindowStyle: null timewindowStyle: null
})); }));
if (this.widgetType === widgetType.alarm) { }
this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null)); if (this.widgetType === widgetType.alarm) {
} this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null));
} }
if (this.modelValue.isDataEnabled) { if (this.modelValue.isDataEnabled) {
if (this.widgetType !== widgetType.rpc && if (this.widgetType !== widgetType.rpc &&
@ -529,14 +533,17 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
}, },
{emitEvent: false} {emitEvent: false}
); );
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { if (widgetTypeCanHaveTimewindow(this.widgetType)) {
const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true; config.useDashboardTimewindow : true;
this.dataSettings.get('timewindowConfig').patchValue({ this.dataSettings.get('timewindowConfig').patchValue({
useDashboardTimewindow, useDashboardTimewindow,
displayTimewindow: isDefined(config.displayTimewindow) ? displayTimewindow: isDefined(config.displayTimewindow) ?
config.displayTimewindow : true, config.displayTimewindow : true,
timewindow: config.timewindow, timewindow: isDefinedAndNotNull(config.timewindow)
? config.timewindow
: initModelFromDefaultTimewindow(null, this.widgetType === widgetType.latest, this.onlyHistoryTimewindow(),
this.timeService, this.widgetType === widgetType.timeseries),
timewindowStyle: config.timewindowStyle timewindowStyle: config.timewindowStyle
}, {emitEvent: false}); }, {emitEvent: false});
} }

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

@ -38,6 +38,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { DashboardWidget } from '@home/models/dashboard-component.models'; import { DashboardWidget } from '@home/models/dashboard-component.models';
import { import {
MobileImageResult,
Widget, Widget,
WidgetAction, WidgetAction,
WidgetActionDescriptor, WidgetActionDescriptor,
@ -126,6 +127,7 @@ import { IModulesMap } from '@modules/common/modules-map.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models'; import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { addDiagnosticChain } from '@angular/compiler-cli/src/ngtsc/diagnostics';
@Component({ @Component({
selector: 'tb-widget', selector: 'tb-widget',
@ -1222,12 +1224,16 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
switch (type) { switch (type) {
case WidgetMobileActionType.takePictureFromGallery: case WidgetMobileActionType.takePictureFromGallery:
case WidgetMobileActionType.takePhoto: case WidgetMobileActionType.takePhoto:
case WidgetMobileActionType.takeScreenshot:
argsObservable = of([mobileAction.saveToGallery]);
break;
case WidgetMobileActionType.scanQrCode: case WidgetMobileActionType.scanQrCode:
case WidgetMobileActionType.getLocation: case WidgetMobileActionType.getLocation:
case WidgetMobileActionType.takeScreenshot:
case WidgetMobileActionType.deviceProvision:
argsObservable = of([]); argsObservable = of([]);
break; break;
case WidgetMobileActionType.deviceProvision:
argsObservable = of([mobileAction.provisionType]);
break;
case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapDirection:
case WidgetMobileActionType.mapLocation: case WidgetMobileActionType.mapLocation:
argsObservable = compileTbFunction(this.http, mobileAction.getLocationFunction, '$event', 'widgetContext', 'entityId', argsObservable = compileTbFunction(this.http, mobileAction.getLocationFunction, '$event', 'widgetContext', 'entityId',
@ -1297,6 +1303,10 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
case WidgetMobileActionType.takePhoto: case WidgetMobileActionType.takePhoto:
case WidgetMobileActionType.takeScreenshot: case WidgetMobileActionType.takeScreenshot:
const imageUrl = actionResult.imageUrl; const imageUrl = actionResult.imageUrl;
if (!additionalParams) {
additionalParams = {};
}
additionalParams.imageInfo = actionResult.imageInfo;
if (isNotEmptyTbFunction(mobileAction.processImageFunction)) { if (isNotEmptyTbFunction(mobileAction.processImageFunction)) {
compileTbFunction(this.http, mobileAction.processImageFunction, 'imageUrl', '$event', 'widgetContext', 'entityId', compileTbFunction(this.http, mobileAction.processImageFunction, 'imageUrl', '$event', 'widgetContext', 'entityId',
'entityName', 'additionalParams', 'entityLabel').subscribe( 'entityName', 'additionalParams', 'entityLabel').subscribe(
@ -1421,6 +1431,23 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
); );
} }
} }
} else if (!this.mobileService.isMobileApp()) {
if (isNotEmptyTbFunction(mobileAction.handleNonMobileFallbackFunction)) {
compileTbFunction(this.http, mobileAction.handleNonMobileFallbackFunction, '$event', 'widgetContext',).subscribe(
{
next: (compiled) => {
try {
compiled.execute($event, this.widgetContext);
} catch (e) {
console.error(e);
}
},
error: (err) => {
console.error(err);
}
}
);
}
} }
} }
); );

4
ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts

@ -29,6 +29,7 @@ import { MobileAppService } from '@core/http/mobile-app.service';
export interface MobileAppDialogData { export interface MobileAppDialogData {
platformType: PlatformType; platformType: PlatformType;
name?: string
} }
@Component({ @Component({
@ -55,6 +56,9 @@ export class MobileAppDialogComponent extends DialogComponent<MobileAppDialogCom
ngAfterViewInit(): void { ngAfterViewInit(): void {
setTimeout(() => { setTimeout(() => {
this.mobileAppComponent.entityForm.markAsDirty(); this.mobileAppComponent.entityForm.markAsDirty();
if (this.data.name) {
this.mobileAppComponent.entityForm.get('title').patchValue(this.data.name, {emitEvent: false});
}
this.mobileAppComponent.entityForm.patchValue({platformType: this.data.platformType}); this.mobileAppComponent.entityForm.patchValue({platformType: this.data.platformType});
this.mobileAppComponent.entityForm.get('platformType').disable({emitEvent: false}); this.mobileAppComponent.entityForm.get('platformType').disable({emitEvent: false});
this.mobileAppComponent.isEdit = true; this.mobileAppComponent.isEdit = true;

4
ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html

@ -58,7 +58,7 @@
labelText="mobile.android-application" labelText="mobile.android-application"
[entityType]="entityType.MOBILE_APP" [entityType]="entityType.MOBILE_APP"
[entitySubtype]="platformType.ANDROID" [entitySubtype]="platformType.ANDROID"
(createNew)="createApplication('androidAppId', platformType.ANDROID)" (createNew)="createApplication($event, 'androidAppId', platformType.ANDROID)"
formControlName="androidAppId"> formControlName="androidAppId">
</tb-entity-autocomplete> </tb-entity-autocomplete>
<tb-entity-autocomplete <tb-entity-autocomplete
@ -68,7 +68,7 @@
labelText="mobile.ios-application" labelText="mobile.ios-application"
[entityType]="entityType.MOBILE_APP" [entityType]="entityType.MOBILE_APP"
[entitySubtype]="platformType.IOS" [entitySubtype]="platformType.IOS"
(createNew)="createApplication('iosAppId', platformType.IOS)" (createNew)="createApplication($event, 'iosAppId', platformType.IOS)"
formControlName="iosAppId"> formControlName="iosAppId">
</tb-entity-autocomplete> </tb-entity-autocomplete>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">

5
ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts

@ -148,12 +148,13 @@ export class MobileBundleDialogComponent extends DialogComponent<MobileBundleDia
} }
} }
createApplication(formControl: string, platformType: PlatformType) { createApplication(name: string, formControl: string, platformType: PlatformType) {
this.dialog.open<MobileAppDialogComponent, MobileAppDialogData, MobileApp>(MobileAppDialogComponent, { this.dialog.open<MobileAppDialogComponent, MobileAppDialogData, MobileApp>(MobileAppDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
platformType platformType,
name
} }
}).afterClosed() }).afterClosed()
.subscribe((app) => { .subscribe((app) => {

5
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html

@ -71,6 +71,11 @@
<span> <span>
{{ noEntitiesMatchingText | translate: {entity: searchText} }} {{ noEntitiesMatchingText | translate: {entity: searchText} }}
</span> </span>
@if (allowCreateNew) {
<span>
<a translate (click)="createNewEntity($event, searchText)">entity.create-new-key</a>
</span>
}
</ng-template> </ng-template>
</div> </div>
</mat-option> </mat-option>

6
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -142,7 +142,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
entityChanged = new EventEmitter<BaseData<EntityId>>(); entityChanged = new EventEmitter<BaseData<EntityId>>();
@Output() @Output()
createNew = new EventEmitter<void>(); createNew = new EventEmitter<string>();
@ViewChild('entityInput', {static: true}) entityInput: ElementRef; @ViewChild('entityInput', {static: true}) entityInput: ElementRef;
@ -451,9 +451,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
return entityType; return entityType;
} }
createNewEntity($event: Event) { createNewEntity($event: Event, searchText?: string) {
$event.stopPropagation(); $event.stopPropagation();
this.createNew.emit(); this.createNew.emit(searchText);
} }
get showEntityLink(): boolean { get showEntityLink(): boolean {

172
ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts

@ -36,7 +36,15 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { deepClone, isDefined, isDefinedAndNotNull, isObject, mergeDeep } from '@core/utils'; import {
deepClean,
deepClone,
deleteFalseProperties,
isDefined,
isDefinedAndNotNull,
isEmpty,
mergeDeepIgnoreArray
} from '@core/utils';
import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@ -152,73 +160,73 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
this.timewindowForm = this.fb.group({ this.timewindowForm = this.fb.group({
selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME], selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME],
realtime: this.fb.group({ realtime: this.fb.group({
realtimeType: [ isDefined(realtime?.realtimeType) ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ], realtimeType: [ isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ],
timewindowMs: [ isDefined(realtime?.timewindowMs) ? this.timewindow.realtime.timewindowMs : null ], timewindowMs: [ isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null ],
interval: [ isDefined(realtime?.interval) ? this.timewindow.realtime.interval : null ], interval: [ isDefined(realtime?.interval) ? realtime.interval : null ],
quickInterval: [ isDefined(realtime?.quickInterval) ? this.timewindow.realtime.quickInterval : null ], quickInterval: [ isDefined(realtime?.quickInterval) ? realtime.quickInterval : null ],
disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomInterval) disableCustomInterval: [ isDefinedAndNotNull(realtime?.disableCustomInterval)
? this.timewindow.realtime?.disableCustomInterval : false ], ? realtime.disableCustomInterval : false ],
disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomGroupInterval) disableCustomGroupInterval: [ isDefinedAndNotNull(realtime?.disableCustomGroupInterval)
? this.timewindow.realtime?.disableCustomGroupInterval : false ], ? realtime.disableCustomGroupInterval : false ],
hideInterval: [ isDefinedAndNotNull(this.timewindow.realtime.hideInterval) hideInterval: [ isDefinedAndNotNull(realtime?.hideInterval)
? this.timewindow.realtime.hideInterval : false ], ? realtime.hideInterval : false ],
hideLastInterval: [{ hideLastInterval: [{
value: isDefinedAndNotNull(this.timewindow.realtime.hideLastInterval) value: isDefinedAndNotNull(realtime?.hideLastInterval)
? this.timewindow.realtime.hideLastInterval : false, ? realtime.hideLastInterval : false,
disabled: this.timewindow.realtime.hideInterval disabled: realtime?.hideInterval
}], }],
hideQuickInterval: [{ hideQuickInterval: [{
value: isDefinedAndNotNull(this.timewindow.realtime.hideQuickInterval) value: isDefinedAndNotNull(realtime?.hideQuickInterval)
? this.timewindow.realtime.hideQuickInterval : false, ? realtime.hideQuickInterval : false,
disabled: this.timewindow.realtime.hideInterval disabled: realtime?.hideInterval
}], }],
advancedParams: this.fb.group({ advancedParams: this.fb.group({
allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedLastIntervals) allowedLastIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedLastIntervals)
? this.timewindow.realtime.advancedParams.allowedLastIntervals : null ], ? realtime.advancedParams.allowedLastIntervals : null ],
allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedQuickIntervals) allowedQuickIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedQuickIntervals)
? this.timewindow.realtime.advancedParams.allowedQuickIntervals : null ], ? realtime.advancedParams.allowedQuickIntervals : null ],
lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.lastAggIntervalsConfig) lastAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.lastAggIntervalsConfig)
? this.timewindow.realtime.advancedParams.lastAggIntervalsConfig : null ], ? realtime.advancedParams.lastAggIntervalsConfig : null ],
quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.quickAggIntervalsConfig) quickAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.quickAggIntervalsConfig)
? this.timewindow.realtime.advancedParams.quickAggIntervalsConfig : null ] ? realtime.advancedParams.quickAggIntervalsConfig : null ]
}) })
}), }),
history: this.fb.group({ history: this.fb.group({
historyType: [ isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL ], historyType: [ isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL ],
timewindowMs: [ isDefined(history?.timewindowMs) ? this.timewindow.history.timewindowMs : null ], timewindowMs: [ isDefined(history?.timewindowMs) ? history.timewindowMs : null ],
interval: [ isDefined(history?.interval) ? this.timewindow.history.interval : null ], interval: [ isDefined(history?.interval) ? history.interval : null ],
fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? this.timewindow.history.fixedTimewindow : null ], fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? history.fixedTimewindow : null ],
quickInterval: [ isDefined(history?.quickInterval) ? this.timewindow.history.quickInterval : null ], quickInterval: [ isDefined(history?.quickInterval) ? history.quickInterval : null ],
disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomInterval) disableCustomInterval: [ isDefinedAndNotNull(history?.disableCustomInterval)
? this.timewindow.history?.disableCustomInterval : false ], ? history.disableCustomInterval : false ],
disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomGroupInterval) disableCustomGroupInterval: [ isDefinedAndNotNull(history?.disableCustomGroupInterval)
? this.timewindow.history?.disableCustomGroupInterval : false ], ? history.disableCustomGroupInterval : false ],
hideInterval: [ isDefinedAndNotNull(this.timewindow.history.hideInterval) hideInterval: [ isDefinedAndNotNull(history?.hideInterval)
? this.timewindow.history.hideInterval : false ], ? history.hideInterval : false ],
hideLastInterval: [{ hideLastInterval: [{
value: isDefinedAndNotNull(this.timewindow.history.hideLastInterval) value: isDefinedAndNotNull(history?.hideLastInterval)
? this.timewindow.history.hideLastInterval : false, ? history.hideLastInterval : false,
disabled: this.timewindow.history.hideInterval disabled: history?.hideInterval
}], }],
hideQuickInterval: [{ hideQuickInterval: [{
value: isDefinedAndNotNull(this.timewindow.history.hideQuickInterval) value: isDefinedAndNotNull(history?.hideQuickInterval)
? this.timewindow.history.hideQuickInterval : false, ? history.hideQuickInterval : false,
disabled: this.timewindow.history.hideInterval disabled: history?.hideInterval
}], }],
hideFixedInterval: [{ hideFixedInterval: [{
value: isDefinedAndNotNull(this.timewindow.history.hideFixedInterval) value: isDefinedAndNotNull(history?.hideFixedInterval)
? this.timewindow.history.hideFixedInterval : false, ? history.hideFixedInterval : false,
disabled: this.timewindow.history.hideInterval disabled: history?.hideInterval
}], }],
advancedParams: this.fb.group({ advancedParams: this.fb.group({
allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedLastIntervals) allowedLastIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedLastIntervals)
? this.timewindow.history.advancedParams.allowedLastIntervals : null ], ? history.advancedParams.allowedLastIntervals : null ],
allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedQuickIntervals) allowedQuickIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedQuickIntervals)
? this.timewindow.history.advancedParams.allowedQuickIntervals : null ], ? history.advancedParams.allowedQuickIntervals : null ],
lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.lastAggIntervalsConfig) lastAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.lastAggIntervalsConfig)
? this.timewindow.history.advancedParams.lastAggIntervalsConfig : null ], ? history.advancedParams.lastAggIntervalsConfig : null ],
quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.quickAggIntervalsConfig) quickAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.quickAggIntervalsConfig)
? this.timewindow.history.advancedParams.quickAggIntervalsConfig : null ] ? history.advancedParams.quickAggIntervalsConfig : null ]
}) })
}), }),
aggregation: this.fb.group({ aggregation: this.fb.group({
@ -423,7 +431,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
update() { update() {
const timewindowFormValue = this.timewindowForm.getRawValue(); const timewindowFormValue = this.timewindowForm.getRawValue();
this.timewindow = mergeDeep(this.timewindow, timewindowFormValue); this.timewindow = mergeDeepIgnoreArray(this.timewindow, timewindowFormValue);
const realtimeConfigurableLastIntervalsAvailable = !(timewindowFormValue.hideAggInterval && const realtimeConfigurableLastIntervalsAvailable = !(timewindowFormValue.hideAggInterval &&
(timewindowFormValue.realtime.hideInterval || timewindowFormValue.realtime.hideLastInterval)); (timewindowFormValue.realtime.hideInterval || timewindowFormValue.realtime.hideLastInterval));
@ -434,82 +442,50 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
const historyConfigurableQuickIntervalsAvailable = !(timewindowFormValue.hideAggInterval && const historyConfigurableQuickIntervalsAvailable = !(timewindowFormValue.hideAggInterval &&
(timewindowFormValue.history.hideInterval || timewindowFormValue.history.hideQuickInterval)); (timewindowFormValue.history.hideInterval || timewindowFormValue.history.hideQuickInterval));
if (realtimeConfigurableLastIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedLastIntervals?.length) { if (!realtimeConfigurableLastIntervalsAvailable) {
this.timewindow.realtime.advancedParams.allowedLastIntervals = timewindowFormValue.realtime.advancedParams.allowedLastIntervals;
} else {
delete this.timewindow.realtime.advancedParams.allowedLastIntervals; delete this.timewindow.realtime.advancedParams.allowedLastIntervals;
} }
if (realtimeConfigurableQuickIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedQuickIntervals?.length) { if (!realtimeConfigurableQuickIntervalsAvailable) {
this.timewindow.realtime.advancedParams.allowedQuickIntervals = timewindowFormValue.realtime.advancedParams.allowedQuickIntervals;
} else {
delete this.timewindow.realtime.advancedParams.allowedQuickIntervals; delete this.timewindow.realtime.advancedParams.allowedQuickIntervals;
} }
if (realtimeConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig) && if (realtimeConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig)) {
Object.keys(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig).length) {
this.timewindow.realtime.advancedParams.lastAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig; this.timewindow.realtime.advancedParams.lastAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig;
} else { } else {
delete this.timewindow.realtime.advancedParams.lastAggIntervalsConfig; delete this.timewindow.realtime.advancedParams.lastAggIntervalsConfig;
} }
if (realtimeConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig) && if (realtimeConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig)) {
Object.keys(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig).length) {
this.timewindow.realtime.advancedParams.quickAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig; this.timewindow.realtime.advancedParams.quickAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig;
} else { } else {
delete this.timewindow.realtime.advancedParams.quickAggIntervalsConfig; delete this.timewindow.realtime.advancedParams.quickAggIntervalsConfig;
} }
if (historyConfigurableLastIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedLastIntervals?.length) { if (!historyConfigurableLastIntervalsAvailable) {
this.timewindow.history.advancedParams.allowedLastIntervals = timewindowFormValue.history.advancedParams.allowedLastIntervals;
} else {
delete this.timewindow.history.advancedParams.allowedLastIntervals; delete this.timewindow.history.advancedParams.allowedLastIntervals;
} }
if (historyConfigurableQuickIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedQuickIntervals?.length) { if (!historyConfigurableQuickIntervalsAvailable) {
this.timewindow.history.advancedParams.allowedQuickIntervals = timewindowFormValue.history.advancedParams.allowedQuickIntervals;
} else {
delete this.timewindow.history.advancedParams.allowedQuickIntervals; delete this.timewindow.history.advancedParams.allowedQuickIntervals;
} }
if (historyConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig) && if (historyConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig)) {
Object.keys(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig).length) {
this.timewindow.history.advancedParams.lastAggIntervalsConfig = timewindowFormValue.history.advancedParams.lastAggIntervalsConfig; this.timewindow.history.advancedParams.lastAggIntervalsConfig = timewindowFormValue.history.advancedParams.lastAggIntervalsConfig;
} else { } else {
delete this.timewindow.history.advancedParams.lastAggIntervalsConfig; delete this.timewindow.history.advancedParams.lastAggIntervalsConfig;
} }
if (historyConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig) && if (historyConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig)) {
Object.keys(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig).length) {
this.timewindow.history.advancedParams.quickAggIntervalsConfig = timewindowFormValue.history.advancedParams.quickAggIntervalsConfig; this.timewindow.history.advancedParams.quickAggIntervalsConfig = timewindowFormValue.history.advancedParams.quickAggIntervalsConfig;
} else { } else {
delete this.timewindow.history.advancedParams.quickAggIntervalsConfig; delete this.timewindow.history.advancedParams.quickAggIntervalsConfig;
} }
if (!Object.keys(this.timewindow.realtime.advancedParams).length) { if (timewindowFormValue.hideAggregation) {
delete this.timewindow.realtime.advancedParams;
}
if (!Object.keys(this.timewindow.history.advancedParams).length) {
delete this.timewindow.history.advancedParams;
}
if (timewindowFormValue.allowedAggTypes?.length && !timewindowFormValue.hideAggregation) {
this.timewindow.allowedAggTypes = timewindowFormValue.allowedAggTypes;
} else {
delete this.timewindow.allowedAggTypes; delete this.timewindow.allowedAggTypes;
} }
if (!this.timewindow.realtime.disableCustomInterval) {
delete this.timewindow.realtime.disableCustomInterval;
}
if (!this.timewindow.realtime.disableCustomGroupInterval) {
delete this.timewindow.realtime.disableCustomGroupInterval;
}
if (!this.timewindow.history.disableCustomInterval) {
delete this.timewindow.history.disableCustomInterval;
}
if (!this.timewindow.history.disableCustomGroupInterval) {
delete this.timewindow.history.disableCustomGroupInterval;
}
if (!this.aggregation) { if (!this.aggregation) {
delete this.timewindow.aggregation; delete this.timewindow.aggregation;
} }
this.dialogRef.close(this.timewindow);
deleteFalseProperties(this.timewindow);
this.dialogRef.close(deepClean(this.timewindow));
} }
cancel() { cancel() {

110
ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts

@ -27,12 +27,14 @@ import {
} from '@angular/core'; } from '@angular/core';
import { import {
AggregationType, AggregationType,
clearTimewindowConfig,
currentHistoryTimewindow, currentHistoryTimewindow,
currentRealtimeTimewindow, currentRealtimeTimewindow,
historyAllowedAggIntervals, historyAllowedAggIntervals,
HistoryWindowType, HistoryWindowType,
historyWindowTypeTranslations, historyWindowTypeTranslations,
Interval, Interval,
MINUTE,
QuickTimeInterval, QuickTimeInterval,
realtimeAllowedAggIntervals, realtimeAllowedAggIntervals,
RealtimeWindowType, RealtimeWindowType,
@ -167,14 +169,14 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
}); });
} }
if ((this.isEdit || !this.timewindow.realtime.hideLastInterval) && !this.quickIntervalOnly) { if ((this.isEdit || !this.timewindow.realtime?.hideLastInterval) && !this.quickIntervalOnly) {
this.realtimeTimewindowOptions.push({ this.realtimeTimewindowOptions.push({
name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.LAST_INTERVAL)), name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.LAST_INTERVAL)),
value: this.realtimeTypes.LAST_INTERVAL value: this.realtimeTypes.LAST_INTERVAL
}); });
} }
if (this.isEdit || !this.timewindow.realtime.hideQuickInterval || this.quickIntervalOnly) { if (this.isEdit || !this.timewindow.realtime?.hideQuickInterval || this.quickIntervalOnly) {
this.realtimeTimewindowOptions.push({ this.realtimeTimewindowOptions.push({
name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.INTERVAL)), name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.INTERVAL)),
value: this.realtimeTypes.INTERVAL value: this.realtimeTypes.INTERVAL
@ -188,21 +190,21 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
}); });
} }
if (this.isEdit || !this.timewindow.history.hideLastInterval) { if (this.isEdit || !this.timewindow.history?.hideLastInterval) {
this.historyTimewindowOptions.push({ this.historyTimewindowOptions.push({
name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.LAST_INTERVAL)), name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.LAST_INTERVAL)),
value: this.historyTypes.LAST_INTERVAL value: this.historyTypes.LAST_INTERVAL
}); });
} }
if (this.isEdit || !this.timewindow.history.hideFixedInterval) { if (this.isEdit || !this.timewindow.history?.hideFixedInterval) {
this.historyTimewindowOptions.push({ this.historyTimewindowOptions.push({
name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.FIXED)), name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.FIXED)),
value: this.historyTypes.FIXED value: this.historyTypes.FIXED
}); });
} }
if (this.isEdit || !this.timewindow.history.hideQuickInterval) { if (this.isEdit || !this.timewindow.history?.hideQuickInterval) {
this.historyTimewindowOptions.push({ this.historyTimewindowOptions.push({
name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.INTERVAL)), name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.INTERVAL)),
value: this.historyTypes.INTERVAL value: this.historyTypes.INTERVAL
@ -211,10 +213,10 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
this.realtimeTypeSelectionAvailable = this.realtimeTimewindowOptions.length > 1; this.realtimeTypeSelectionAvailable = this.realtimeTimewindowOptions.length > 1;
this.historyTypeSelectionAvailable = this.historyTimewindowOptions.length > 1; this.historyTypeSelectionAvailable = this.historyTimewindowOptions.length > 1;
this.realtimeIntervalSelectionAvailable = this.isEdit || !(this.timewindow.realtime.hideInterval || this.realtimeIntervalSelectionAvailable = this.isEdit || !(this.timewindow.realtime?.hideInterval ||
(this.timewindow.realtime.hideLastInterval && this.timewindow.realtime.hideQuickInterval)); (this.timewindow.realtime?.hideLastInterval && this.timewindow.realtime?.hideQuickInterval));
this.historyIntervalSelectionAvailable = this.isEdit || !(this.timewindow.history.hideInterval || this.historyIntervalSelectionAvailable = this.isEdit || !(this.timewindow.history?.hideInterval ||
(this.timewindow.history.hideLastInterval && this.timewindow.history.hideQuickInterval && this.timewindow.history.hideFixedInterval)); (this.timewindow.history?.hideLastInterval && this.timewindow.history?.hideQuickInterval && this.timewindow.history?.hideFixedInterval));
this.aggregationOptionsAvailable = this.aggregation && (this.isEdit || this.aggregationOptionsAvailable = this.aggregation && (this.isEdit ||
!(this.timewindow.hideAggregation && this.timewindow.hideAggInterval)); !(this.timewindow.hideAggregation && this.timewindow.hideAggInterval));
@ -230,28 +232,28 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
const aggregation = this.timewindow.aggregation; const aggregation = this.timewindow.aggregation;
if (!this.isEdit) { if (!this.isEdit) {
if (realtime.hideLastInterval && !realtime.hideQuickInterval) { if (realtime?.hideLastInterval && !realtime?.hideQuickInterval) {
realtime.realtimeType = RealtimeWindowType.INTERVAL; realtime.realtimeType = RealtimeWindowType.INTERVAL;
} }
if (realtime.hideQuickInterval && !realtime.hideLastInterval) { if (realtime?.hideQuickInterval && !realtime?.hideLastInterval) {
realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL; realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL;
} }
if (history.hideLastInterval) { if (history?.hideLastInterval) {
if (!history.hideFixedInterval) { if (!history.hideFixedInterval) {
history.historyType = HistoryWindowType.FIXED; history.historyType = HistoryWindowType.FIXED;
} else if (!history.hideQuickInterval) { } else if (!history.hideQuickInterval) {
history.historyType = HistoryWindowType.INTERVAL; history.historyType = HistoryWindowType.INTERVAL;
} }
} }
if (history.hideFixedInterval) { if (history?.hideFixedInterval) {
if (!history.hideLastInterval) { if (!history.hideLastInterval) {
history.historyType = HistoryWindowType.LAST_INTERVAL; history.historyType = HistoryWindowType.LAST_INTERVAL;
} else if (!history.hideQuickInterval) { } else if (!history.hideQuickInterval) {
history.historyType = HistoryWindowType.INTERVAL; history.historyType = HistoryWindowType.INTERVAL;
} }
} }
if (history.hideQuickInterval) { if (history?.hideQuickInterval) {
if (!history.hideLastInterval) { if (!history.hideLastInterval) {
history.historyType = HistoryWindowType.LAST_INTERVAL; history.historyType = HistoryWindowType.LAST_INTERVAL;
} else if (!history.hideFixedInterval) { } else if (!history.hideFixedInterval) {
@ -265,29 +267,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
realtime: this.fb.group({ realtime: this.fb.group({
realtimeType: [{ realtimeType: [{
value: isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, value: isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL,
disabled: realtime.hideInterval disabled: realtime?.hideInterval
}], }],
timewindowMs: [{ timewindowMs: [{
value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null, value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : MINUTE,
disabled: realtime.hideInterval || realtime.hideLastInterval disabled: realtime?.hideInterval || realtime?.hideLastInterval
}], }],
interval: [{ interval: [{
value:isDefined(realtime?.interval) ? realtime.interval : null, value:isDefined(realtime?.interval) ? realtime.interval : null,
disabled: hideAggInterval disabled: hideAggInterval
}], }],
quickInterval: [{ quickInterval: [{
value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : null, value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : QuickTimeInterval.CURRENT_DAY,
disabled: realtime.hideInterval || realtime.hideQuickInterval disabled: realtime?.hideInterval || realtime?.hideQuickInterval
}] }]
}), }),
history: this.fb.group({ history: this.fb.group({
historyType: [{ historyType: [{
value: isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL, value: isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL,
disabled: history.hideInterval disabled: history?.hideInterval
}], }],
timewindowMs: [{ timewindowMs: [{
value: isDefined(history?.timewindowMs) ? history.timewindowMs : null, value: isDefined(history?.timewindowMs) ? history.timewindowMs : MINUTE,
disabled: history.hideInterval || history.hideLastInterval disabled: history?.hideInterval || history?.hideLastInterval
}], }],
interval: [{ interval: [{
value:isDefined(history?.interval) ? history.interval : null, value:isDefined(history?.interval) ? history.interval : null,
@ -296,11 +298,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
fixedTimewindow: [{ fixedTimewindow: [{
value: isDefined(history?.fixedTimewindow) && this.timewindow.selectedTab === TimewindowType.HISTORY value: isDefined(history?.fixedTimewindow) && this.timewindow.selectedTab === TimewindowType.HISTORY
&& history.historyType === HistoryWindowType.FIXED ? history.fixedTimewindow : null, && history.historyType === HistoryWindowType.FIXED ? history.fixedTimewindow : null,
disabled: history.hideInterval || history.hideFixedInterval disabled: history?.hideInterval || history?.hideFixedInterval
}], }],
quickInterval: [{ quickInterval: [{
value: isDefined(history?.quickInterval) ? history.quickInterval : null, value: isDefined(history?.quickInterval) ? history.quickInterval : QuickTimeInterval.CURRENT_DAY,
disabled: history.hideInterval || history.hideQuickInterval disabled: history?.hideInterval || history?.hideQuickInterval
}] }]
}), }),
aggregation: this.fb.group({ aggregation: this.fb.group({
@ -379,8 +381,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
this.timewindowForm.valueChanges.pipe( this.timewindowForm.valueChanges.pipe(
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe(() => { ).subscribe(() => {
this.prepareTimewindowConfig(); this.changeTimewindow.emit(this.prepareTimewindowConfig());
this.changeTimewindow.emit(this.timewindow);
}); });
} }
} }
@ -406,27 +407,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
} }
update() { update() {
this.prepareTimewindowConfig(); this.result = this.prepareTimewindowConfig();
this.result = this.timewindow;
this.overlayRef?.dispose(); this.overlayRef?.dispose();
} }
private prepareTimewindowConfig() { private prepareTimewindowConfig(clearConfig = true): Timewindow {
const timewindowFormValue = this.timewindowForm.getRawValue(); const timewindowFormValue = this.timewindowForm.getRawValue();
this.timewindow.selectedTab = timewindowFormValue.selectedTab; this.timewindow.selectedTab = timewindowFormValue.selectedTab;
this.timewindow.realtime = {...this.timewindow.realtime, ...{ if (this.timewindow.selectedTab === TimewindowType.REALTIME) {
realtimeType: timewindowFormValue.realtime.realtimeType, this.timewindow.realtime = {...this.timewindow.realtime, ...{
timewindowMs: timewindowFormValue.realtime.timewindowMs, realtimeType: timewindowFormValue.realtime.realtimeType,
quickInterval: timewindowFormValue.realtime.quickInterval, timewindowMs: timewindowFormValue.realtime.timewindowMs,
interval: timewindowFormValue.realtime.interval quickInterval: timewindowFormValue.realtime.quickInterval,
}}; interval: timewindowFormValue.realtime.interval,
this.timewindow.history = {...this.timewindow.history, ...{ }};
historyType: timewindowFormValue.history.historyType, } else {
timewindowMs: timewindowFormValue.history.timewindowMs, this.timewindow.history = {...this.timewindow.history, ...{
interval: timewindowFormValue.history.interval, historyType: timewindowFormValue.history.historyType,
fixedTimewindow: timewindowFormValue.history.fixedTimewindow, timewindowMs: timewindowFormValue.history.timewindowMs,
quickInterval: timewindowFormValue.history.quickInterval, fixedTimewindow: timewindowFormValue.history.fixedTimewindow,
}}; quickInterval: timewindowFormValue.history.quickInterval,
interval: timewindowFormValue.history.interval,
}};
}
if (this.aggregation) { if (this.aggregation) {
this.timewindow.aggregation = { this.timewindow.aggregation = {
@ -437,6 +440,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
if (this.timezone) { if (this.timezone) {
this.timewindow.timezone = timewindowFormValue.timezone; this.timewindow.timezone = timewindowFormValue.timezone;
} }
if (clearConfig) {
return clearTimewindowConfig(this.timewindow, this.quickIntervalOnly, this.historyOnly, this.aggregation, this.timezone);
} else {
return deepClone(this.timewindow);
}
} }
private updateTimewindowForm() { private updateTimewindowForm() {
@ -546,7 +555,6 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
} }
openTimewindowConfig() { openTimewindowConfig() {
this.prepareTimewindowConfig();
this.dialog.open<TimewindowConfigDialogComponent, TimewindowConfigDialogData, Timewindow>( this.dialog.open<TimewindowConfigDialogComponent, TimewindowConfigDialogData, Timewindow>(
TimewindowConfigDialogComponent, { TimewindowConfigDialogComponent, {
autoFocus: false, autoFocus: false,
@ -555,7 +563,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
data: { data: {
quickIntervalOnly: this.quickIntervalOnly, quickIntervalOnly: this.quickIntervalOnly,
aggregation: this.aggregation, aggregation: this.aggregation,
timewindow: deepClone(this.timewindow) timewindow: this.prepareTimewindowConfig(false)
} }
}).afterClosed() }).afterClosed()
.subscribe((res) => { .subscribe((res) => {
@ -568,12 +576,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
} }
private updateTimewindowAdvancedParams() { private updateTimewindowAdvancedParams() {
this.realtimeDisableCustomInterval = this.timewindow.realtime.disableCustomInterval; this.realtimeDisableCustomInterval = this.timewindow.realtime?.disableCustomInterval;
this.realtimeDisableCustomGroupInterval = this.timewindow.realtime.disableCustomGroupInterval; this.realtimeDisableCustomGroupInterval = this.timewindow.realtime?.disableCustomGroupInterval;
this.historyDisableCustomInterval = this.timewindow.history.disableCustomInterval; this.historyDisableCustomInterval = this.timewindow.history?.disableCustomInterval;
this.historyDisableCustomGroupInterval = this.timewindow.history.disableCustomGroupInterval; this.historyDisableCustomGroupInterval = this.timewindow.history?.disableCustomGroupInterval;
if (this.timewindow.realtime.advancedParams) { if (this.timewindow.realtime?.advancedParams) {
this.realtimeAdvancedParams = this.timewindow.realtime.advancedParams; this.realtimeAdvancedParams = this.timewindow.realtime.advancedParams;
this.realtimeAllowedLastIntervals = this.timewindow.realtime.advancedParams.allowedLastIntervals; this.realtimeAllowedLastIntervals = this.timewindow.realtime.advancedParams.allowedLastIntervals;
this.realtimeAllowedQuickIntervals = this.timewindow.realtime.advancedParams.allowedQuickIntervals; this.realtimeAllowedQuickIntervals = this.timewindow.realtime.advancedParams.allowedQuickIntervals;
@ -582,7 +590,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O
this.realtimeAllowedLastIntervals = null; this.realtimeAllowedLastIntervals = null;
this.realtimeAllowedQuickIntervals = null; this.realtimeAllowedQuickIntervals = null;
} }
if (this.timewindow.history.advancedParams) { if (this.timewindow.history?.advancedParams) {
this.historyAdvancedParams = this.timewindow.history.advancedParams; this.historyAdvancedParams = this.timewindow.history.advancedParams;
this.historyAllowedLastIntervals = this.timewindow.history.advancedParams.allowedLastIntervals; this.historyAllowedLastIntervals = this.timewindow.history.advancedParams.allowedLastIntervals;
this.historyAllowedQuickIntervals = this.timewindow.history.advancedParams.allowedQuickIntervals; this.historyAllowedQuickIntervals = this.timewindow.history.advancedParams.allowedQuickIntervals;

3
ui-ngx/src/app/shared/components/time/timewindow.component.ts

@ -319,7 +319,8 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan
} }
writeValue(obj: Timewindow): void { writeValue(obj: Timewindow): void {
this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.historyOnly, this.timeService); this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.historyOnly, this.timeService,
this.aggregation);
this.timewindowDisabled = this.isTimewindowDisabled(); this.timewindowDisabled = this.isTimewindowDisabled();
if (this.onHistoryOnlyChanged()) { if (this.onHistoryOnlyChanged()) {
setTimeout(() => { setTimeout(() => {

5
ui-ngx/src/app/shared/models/alarm.models.ts

@ -271,6 +271,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = {
keyName: 'assignee', keyName: 'assignee',
value: 'assignee', value: 'assignee',
name: 'alarm.assignee' name: 'alarm.assignee'
},
details: {
keyName: 'details',
value: 'details',
name: 'alarm.details'
} }
}; };

140
ui-ngx/src/app/shared/models/time/time.models.ts

@ -15,7 +15,7 @@
/// ///
import { TimeService } from '@core/services/time.service'; import { TimeService } from '@core/services/time.service';
import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } from '@app/core/utils'; import { deepClean, deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } from '@app/core/utils';
import moment_ from 'moment'; import moment_ from 'moment';
import * as momentTz from 'moment-timezone'; import * as momentTz from 'moment-timezone';
import { IntervalType } from '@shared/models/telemetry/telemetry.models'; import { IntervalType } from '@shared/models/telemetry/telemetry.models';
@ -281,19 +281,12 @@ export const historyInterval = (timewindowMs: number): Timewindow => ({
export const defaultTimewindow = (timeService: TimeService): Timewindow => { export const defaultTimewindow = (timeService: TimeService): Timewindow => {
const currentTime = moment().valueOf(); const currentTime = moment().valueOf();
return { return {
displayValue: '',
hideAggregation: false,
hideAggInterval: false,
hideTimezone: false,
selectedTab: TimewindowType.REALTIME, selectedTab: TimewindowType.REALTIME,
realtime: { realtime: {
realtimeType: RealtimeWindowType.LAST_INTERVAL, realtimeType: RealtimeWindowType.LAST_INTERVAL,
interval: SECOND, interval: SECOND,
timewindowMs: MINUTE, timewindowMs: MINUTE,
quickInterval: QuickTimeInterval.CURRENT_DAY, quickInterval: QuickTimeInterval.CURRENT_DAY,
hideInterval: false,
hideLastInterval: false,
hideQuickInterval: false
}, },
history: { history: {
historyType: HistoryWindowType.LAST_INTERVAL, historyType: HistoryWindowType.LAST_INTERVAL,
@ -304,10 +297,6 @@ export const defaultTimewindow = (timeService: TimeService): Timewindow => {
endTimeMs: currentTime endTimeMs: currentTime
}, },
quickInterval: QuickTimeInterval.CURRENT_DAY, quickInterval: QuickTimeInterval.CURRENT_DAY,
hideInterval: false,
hideLastInterval: false,
hideFixedInterval: false,
hideQuickInterval: false
}, },
aggregation: { aggregation: {
type: AggregationType.AVG, type: AggregationType.AVG,
@ -325,40 +314,47 @@ const getTimewindowType = (timewindow: Timewindow): TimewindowType => {
}; };
export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean,
historyOnly: boolean, timeService: TimeService): Timewindow => { historyOnly: boolean, timeService: TimeService, hasAggregation: boolean): Timewindow => {
const model = defaultTimewindow(timeService); const model = defaultTimewindow(timeService);
if (value) { if (value) {
if (value.allowedAggTypes?.length) { if (value.allowedAggTypes?.length) {
model.allowedAggTypes = value.allowedAggTypes; model.allowedAggTypes = value.allowedAggTypes;
} }
model.hideAggregation = value.hideAggregation; if (value.hideAggregation) {
model.hideAggInterval = value.hideAggInterval; model.hideAggregation = value.hideAggregation;
model.hideTimezone = value.hideTimezone; }
if (value.hideAggInterval) {
model.hideAggInterval = value.hideAggInterval;
}
if (value.hideTimezone) {
model.hideTimezone = value.hideTimezone;
}
model.selectedTab = getTimewindowType(value); model.selectedTab = getTimewindowType(value);
// for backward compatibility // for backward compatibility
if (isDefinedAndNotNull((value as any).hideInterval)) { if ((value as any).hideInterval) {
model.realtime.hideInterval = (value as any).hideInterval; model.realtime.hideInterval = (value as any).hideInterval;
model.history.hideInterval = (value as any).hideInterval; model.history.hideInterval = (value as any).hideInterval;
delete (value as any).hideInterval; delete (value as any).hideInterval;
} }
if (isDefinedAndNotNull((value as any).hideLastInterval)) { if ((value as any).hideLastInterval) {
model.realtime.hideLastInterval = (value as any).hideLastInterval; model.realtime.hideLastInterval = (value as any).hideLastInterval;
delete (value as any).hideLastInterval; delete (value as any).hideLastInterval;
} }
if (isDefinedAndNotNull((value as any).hideQuickInterval)) { if ((value as any).hideQuickInterval) {
model.realtime.hideQuickInterval = (value as any).hideQuickInterval; model.realtime.hideQuickInterval = (value as any).hideQuickInterval;
delete (value as any).hideQuickInterval; delete (value as any).hideQuickInterval;
} }
if (isDefined(value.realtime)) { if (isDefined(value.realtime)) {
if (isDefinedAndNotNull(value.realtime.hideInterval)) { if (value.realtime.hideInterval) {
model.realtime.hideInterval = value.realtime.hideInterval; model.realtime.hideInterval = value.realtime.hideInterval;
} }
if (isDefinedAndNotNull(value.realtime.hideLastInterval)) { if (value.realtime.hideLastInterval) {
model.realtime.hideLastInterval = value.realtime.hideLastInterval; model.realtime.hideLastInterval = value.realtime.hideLastInterval;
} }
if (isDefinedAndNotNull(value.realtime.hideQuickInterval)) { if (value.realtime.hideQuickInterval) {
model.realtime.hideQuickInterval = value.realtime.hideQuickInterval; model.realtime.hideQuickInterval = value.realtime.hideQuickInterval;
} }
if (value.realtime.disableCustomInterval) { if (value.realtime.disableCustomInterval) {
@ -392,16 +388,16 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
} }
} }
if (isDefined(value.history)) { if (isDefined(value.history)) {
if (isDefinedAndNotNull(value.history.hideInterval)) { if (value.history.hideInterval) {
model.history.hideInterval = value.history.hideInterval; model.history.hideInterval = value.history.hideInterval;
} }
if (isDefinedAndNotNull(value.history.hideLastInterval)) { if (value.history.hideLastInterval) {
model.history.hideLastInterval = value.history.hideLastInterval; model.history.hideLastInterval = value.history.hideLastInterval;
} }
if (isDefinedAndNotNull(value.history.hideFixedInterval)) { if (value.history.hideFixedInterval) {
model.history.hideFixedInterval = value.history.hideFixedInterval; model.history.hideFixedInterval = value.history.hideFixedInterval;
} }
if (isDefinedAndNotNull(value.history.hideQuickInterval)) { if (value.history.hideQuickInterval) {
model.history.hideQuickInterval = value.history.hideQuickInterval; model.history.hideQuickInterval = value.history.hideQuickInterval;
} }
if (value.history.disableCustomInterval) { if (value.history.disableCustomInterval) {
@ -450,7 +446,9 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
} }
model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2);
} }
model.timezone = value.timezone; if (value.timezone) {
model.timezone = value.timezone;
}
} }
if (quickIntervalOnly) { if (quickIntervalOnly) {
model.realtime.realtimeType = RealtimeWindowType.INTERVAL; model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
@ -458,7 +456,7 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
if (historyOnly) { if (historyOnly) {
model.selectedTab = TimewindowType.HISTORY; model.selectedTab = TimewindowType.HISTORY;
} }
return model; return clearTimewindowConfig(model, quickIntervalOnly, historyOnly, hasAggregation);
}; };
export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number, export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number,
@ -1098,19 +1096,91 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => {
if (timewindow.allowedAggTypes?.length) { if (timewindow.allowedAggTypes?.length) {
cloned.allowedAggTypes = timewindow.allowedAggTypes; cloned.allowedAggTypes = timewindow.allowedAggTypes;
} }
cloned.hideAggregation = timewindow.hideAggregation || false; if (timewindow.hideAggregation) {
cloned.hideAggInterval = timewindow.hideAggInterval || false; cloned.hideAggregation = timewindow.hideAggregation;
cloned.hideTimezone = timewindow.hideTimezone || false; }
if (timewindow.hideAggInterval) {
cloned.hideAggInterval = timewindow.hideAggInterval;
}
if (timewindow.hideTimezone) {
cloned.hideTimezone = timewindow.hideTimezone;
}
if (isDefined(timewindow.selectedTab)) { if (isDefined(timewindow.selectedTab)) {
cloned.selectedTab = timewindow.selectedTab; cloned.selectedTab = timewindow.selectedTab;
} }
cloned.realtime = deepClone(timewindow.realtime); if (isDefined(timewindow.realtime)) {
cloned.history = deepClone(timewindow.history); cloned.realtime = deepClone(timewindow.realtime);
cloned.aggregation = deepClone(timewindow.aggregation); }
cloned.timezone = timewindow.timezone; if (isDefined(timewindow.history)) {
cloned.history = deepClone(timewindow.history);
}
if (isDefined(timewindow.aggregation)) {
cloned.aggregation = deepClone(timewindow.aggregation);
}
if (timewindow.timezone) {
cloned.timezone = timewindow.timezone;
}
return cloned; return cloned;
}; };
export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: boolean,
historyOnly: boolean, hasAggregation: boolean, hasTimezone = true): Timewindow => {
if (timewindow.selectedTab === TimewindowType.REALTIME) {
if (quickIntervalOnly || timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL) {
delete timewindow.realtime.timewindowMs;
} else {
delete timewindow.realtime.quickInterval;
}
delete timewindow.history?.historyType;
delete timewindow.history?.timewindowMs;
delete timewindow.history?.fixedTimewindow;
delete timewindow.history?.quickInterval;
delete timewindow.history?.interval;
if (!hasAggregation) {
delete timewindow.realtime.interval;
}
} else {
if (timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) {
delete timewindow.history.fixedTimewindow;
delete timewindow.history.quickInterval;
} else if (timewindow.history.historyType === HistoryWindowType.FIXED) {
delete timewindow.history.timewindowMs;
delete timewindow.history.quickInterval;
} else if (timewindow.history.historyType === HistoryWindowType.INTERVAL) {
delete timewindow.history.timewindowMs;
delete timewindow.history.fixedTimewindow;
} else {
delete timewindow.history.timewindowMs;
delete timewindow.history.fixedTimewindow;
delete timewindow.history.quickInterval;
}
delete timewindow.realtime?.realtimeType;
delete timewindow.realtime?.timewindowMs;
delete timewindow.realtime?.quickInterval;
delete timewindow.realtime?.interval;
if (!hasAggregation) {
delete timewindow.history.interval;
}
}
if (!hasAggregation) {
delete timewindow.aggregation;
}
if (historyOnly) {
delete timewindow.realtime;
}
if (!hasTimezone) {
delete timewindow.timezone;
}
return deepClean(timewindow);
};
export interface TimeInterval { export interface TimeInterval {
name: string; name: string;
translateParams: {[key: string]: any}; translateParams: {[key: string]: any};

39
ui-ngx/src/app/shared/models/widget.models.ts

@ -33,7 +33,7 @@ import { PageComponent } from '@shared/components/page.component';
import { AfterViewInit, DestroyRef, Directive, EventEmitter, inject, Inject, OnInit, Type } from '@angular/core'; import { AfterViewInit, DestroyRef, Directive, EventEmitter, inject, Inject, OnInit, Type } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, UntypedFormGroup, ValidatorFn } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { IAliasController } from '@core/api/widget-api.models'; import { IAliasController } from '@core/api/widget-api.models';
@ -51,6 +51,7 @@ import { TbFunction } from '@shared/models/js-function.models';
import { FormProperty, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models'; import { FormProperty, jsonFormSchemaToFormProperties } from '@shared/models/dynamic-form.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { TbUnit } from '@shared/models/unit.models'; import { TbUnit } from '@shared/models/unit.models';
import { ImageResourceInfo } from '@shared/models/resource.models';
export enum widgetType { export enum widgetType {
timeseries = 'timeseries', timeseries = 'timeseries',
@ -489,6 +490,14 @@ export const targetDeviceValid = (targetDevice?: TargetDevice): boolean =>
((targetDevice.type === TargetDeviceType.device && !!targetDevice.deviceId) || ((targetDevice.type === TargetDeviceType.device && !!targetDevice.deviceId) ||
(targetDevice.type === TargetDeviceType.entity && !!targetDevice.entityAliasId)); (targetDevice.type === TargetDeviceType.entity && !!targetDevice.entityAliasId));
export const widgetTypeHasTimewindow = (type: widgetType): boolean => {
return type === widgetType.timeseries || type === widgetType.alarm;
}
export const widgetTypeCanHaveTimewindow = (type: widgetType): boolean => {
return widgetTypeHasTimewindow(type) || type === widgetType.latest;
}
export const datasourcesHasAggregation = (datasources?: Array<Datasource>): boolean => { export const datasourcesHasAggregation = (datasources?: Array<Datasource>): boolean => {
if (datasources) { if (datasources) {
const foundDatasource = datasources.find(datasource => { const foundDatasource = datasources.find(datasource => {
@ -622,6 +631,30 @@ export enum WidgetMobileActionType {
deviceProvision = 'deviceProvision', deviceProvision = 'deviceProvision',
} }
export interface ActionConfig {
title: string,
formControlName: string,
functionName: string,
functionArgs: string[],
helpId?: string
}
export enum ProvisionType {
auto = 'auto',
wiFi = 'wiFi',
ble = 'ble',
softAp = 'softAp'
}
export const provisionTypeTranslationMap = new Map<ProvisionType, string>(
[
[ ProvisionType.auto, 'widget-action.mobile.auto' ],
[ ProvisionType.wiFi, 'widget-action.mobile.wi-fi' ],
[ ProvisionType.ble, 'widget-action.mobile.ble' ],
[ ProvisionType.softAp, 'widget-action.mobile.soft-ap' ],
]
);
export enum MapItemType { export enum MapItemType {
marker = 'marker', marker = 'marker',
polygon = 'polygon', polygon = 'polygon',
@ -675,6 +708,7 @@ export interface MobileLaunchResult {
export interface MobileImageResult { export interface MobileImageResult {
imageUrl: string; imageUrl: string;
imageInfo?: ImageResourceInfo;
} }
export interface MobileQrCodeResult { export interface MobileQrCodeResult {
@ -706,10 +740,12 @@ export interface WidgetMobileActionResult<T extends MobileActionResult> {
export interface ProvisionSuccessDescriptor { export interface ProvisionSuccessDescriptor {
handleProvisionSuccessFunction: TbFunction; handleProvisionSuccessFunction: TbFunction;
provisionType?: string;
} }
export interface ProcessImageDescriptor { export interface ProcessImageDescriptor {
processImageFunction: TbFunction; processImageFunction: TbFunction;
saveToGallery?: boolean;
} }
export interface ProcessLaunchResultDescriptor { export interface ProcessLaunchResultDescriptor {
@ -743,6 +779,7 @@ export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescript
type: WidgetMobileActionType; type: WidgetMobileActionType;
handleErrorFunction?: TbFunction; handleErrorFunction?: TbFunction;
handleEmptyResultFunction?: TbFunction; handleEmptyResultFunction?: TbFunction;
handleNonMobileFallbackFunction?: TbFunction;
} }
export interface CustomActionDescriptor { export interface CustomActionDescriptor {

67
ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts

@ -17,13 +17,18 @@
import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models';
import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models'; import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models';
import { Dashboard } from '@shared/models/dashboard.models'; import { Dashboard } from '@shared/models/dashboard.models';
import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models'; import { DataKey, Datasource, datasourcesHasAggregation, DatasourceType, Widget } from '@shared/models/widget.models';
import { import {
additionalMapDataSourcesToDatasources,
BaseMapSettings, BaseMapSettings,
CirclesDataLayerSettings,
MapDataLayerSettings, MapDataLayerSettings,
MapDataLayerType,
MapDataSourceSettings, MapDataSourceSettings,
mapDataSourceSettingsToDatasource, mapDataSourceSettingsToDatasource,
MapType MapType,
MarkersDataLayerSettings,
PolygonsDataLayerSettings
} from '@shared/models/widget/maps/map.models'; } from '@shared/models/widget/maps/map.models';
import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
@ -124,6 +129,27 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources)); datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources));
} }
return datasources; return datasources;
},
hasTimewindow(widget: Widget): boolean {
const settings: BaseMapSettings = widget.config.settings as BaseMapSettings;
if (settings.trips?.length) {
return true;
} else {
const datasources: Datasource[] = [];
if (settings.markers?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.markers, 'markers'));
}
if (settings.polygons?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.polygons, 'polygons'));
}
if (settings.circles?.length) {
datasources.push(...getMapLatestDataLayersDatasources(settings.circles, 'circles'));
}
if (settings.additionalDataSources?.length) {
datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources));
}
return datasourcesHasAggregation(datasources);
}
} }
}; };
@ -223,3 +249,40 @@ const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataS
}); });
return datasources; return datasources;
}; };
const getMapLatestDataLayersDatasources = (settings: MapDataLayerSettings[],
dataLayerType: MapDataLayerType): Datasource[] => {
const datasources: Datasource[] = [];
settings.forEach((dsSettings) => {
const dataKeys: DataKey[] = getMapLatestDataLayerDatasourceDataKeys(dsSettings, dataLayerType);
const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings);
datasource.dataKeys.push(...dataKeys);
datasources.push(datasource);
if ((dsSettings).additionalDataSources?.length) {
(dsSettings).additionalDataSources.forEach((ds) => {
const additionalDatasource: Datasource = mapDataSourceSettingsToDatasource(ds);
additionalDatasource.dataKeys.push(...dataKeys);
datasources.push(additionalDatasource);
});
}
});
return datasources;
};
const getMapLatestDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings,
dataLayerType: MapDataLayerType): DataKey[] => {
const dataKeys = settings.additionalDataKeys || [];
switch (dataLayerType) {
case 'markers':
const markersSettings = settings as MarkersDataLayerSettings;
dataKeys.push(markersSettings.xKey, markersSettings.yKey);
break;
case 'polygons':
dataKeys.push((settings as PolygonsDataLayerSettings).polygonKey);
break;
case 'circles':
dataKeys.push((settings as CirclesDataLayerSettings).circleKey);
break;
}
return dataKeys;
};

1
ui-ngx/src/app/shared/models/widget/widget-model.definition.ts

@ -25,6 +25,7 @@ export interface WidgetModelDefinition<T = any> {
prepareExportInfo(dashboard: Dashboard, widget: Widget): T; prepareExportInfo(dashboard: Dashboard, widget: Widget): T;
updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void; updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void;
datasources(widget: Widget): Datasource[]; datasources(widget: Widget): Datasource[];
hasTimewindow(widget: Widget): boolean;
} }
const widgetModelRegistry: WidgetModelDefinition[] = [ const widgetModelRegistry: WidgetModelDefinition[] = [

171
ui-ngx/src/assets/help/en_US/rulenode/ai_node_prompt_settings.md

@ -1,3 +1,174 @@
#### Example Usage: AI-Powered Predictive Maintenance
This example demonstrates how to use the AI request node to analyze telemetry from rotating equipment for a predictive maintenance use case.
##### Scenario
Assume you’re monitoring a centrifugal pump that streams vibration, temperature, and acoustic readings.
To catch problems early and avoid downtime, you can use AI to analyze the telemetry for signs of **Bearing Wear**, **Misalignment**, **Overheating**, or **Imbalance** and return an alarm object if found. Downstream nodes can use it to create a ThingsBoard alarm and notify the maintenance team.
1. **Incoming message structure**
First, we need to collect telemetry readings. This can be achieved either by configuring a Calculated Field with the “Time series rolling” arguments to gather recent samples,
or by running a periodic check using nodes like "generator" and "originator telemetry" to fetch the latest samples and assemble the payload.
Message payload (shortened for brevity):
```json
{
"acousticDeviation": {
"timeWindow": { "startTs": 1755093373000, "endTs": 1755093414551 },
"values": [
{ "ts": 1755093373000, "value": 5.0 },
{ "ts": 1755093373100, "value": 18.0 },
{ "ts": 1755093373200, "value": 17.0 },
{ "ts": 1755093414380, "value": 5.0 },
{ "ts": 1755093414551, "value": 17.0 }
]
},
"temperature": {
"timeWindow": { "startTs": 1755093373000, "endTs": 1755093414551 },
"values": [
{ "ts": 1755093373000, "value": 70.0 },
{ "ts": 1755093373120, "value": 86.0 },
{ "ts": 1755093373200, "value": 84.0 },
{ "ts": 1755093414380, "value": 70.0 },
{ "ts": 1755093414551, "value": 84.0 }
]
},
"vibration": {
"timeWindow": { "startTs": 1755093373000, "endTs": 1755093414551 },
"values": [
{ "ts": 1755093373000, "value": 4.2 },
{ "ts": 1755093373120, "value": 7.4 },
{ "ts": 1755093373182, "value": 8.0 },
{ "ts": 1755093414437, "value": 6.2 },
{ "ts": 1755093414551, "value": 7.2 }
]
}
}
```
Message metadata:
```json
{
"deviceName": "Pump-103",
"deviceType": "CentrifugalPump"
}
```
2. **Prompt configuration**
As a second step, we need to explain the task to AI model. Describe the context of your device and the desired response format (in this case, minimal ThingsBoard alarm JSON object) in the system prompt. We will also put the task description in the system prompt since it does not change depending on a message. In the user prompt, we will use templates to dynamically inject telemetry data produced by the device.
**System prompt**
```
You are an AI predictive maintenance assistant that detects alarm conditions in telemetry data of industrial devices based on incident patterns.
Output JSON only. If an alarm condition is detected, output:
{
"type": "Bearing Wear | Misalignment | Overheating | Imbalance",
"severity": "CRITICAL | MAJOR | MINOR | WARNING",
"details": { "summary": "2–3 sentences in plain English of concise, plain-language summary for maintenance teams; include units when citing values." }
}
If no alarm condition is detected, output: {}
Inputs: time-stamped vibration (mm/s), temperature (°C), acoustic spectrum deviation (%).
Telemetry thresholds:
- Vibration (mm/s): ≤4.5 normal; 4.5–5.0 WARNING; 5.0–6.0 MINOR; 6.0–7.1 MAJOR; >7.1 CRITICAL
- Temperature (°C): ≤75 normal; 75–80 WARNING; 80–85 MAJOR; >85 CRITICAL
- Acoustic deviation (%): ≤15 normal; 15–25 WARNING; 25–40 MINOR; 40–60 MAJOR; >60 CRITICAL
Incident patterns:
- Bearing Wear: gradual vibration rise + temperature spike.
- Misalignment: sudden vibration spike without temperature change.
- Imbalance: rising vibration + irregular acoustics; temperature near normal.
- Overheating: temperature >85 °C ≥10 min or Δtemp ≥10 °C/10 min with Δvib <1.0 mm/s; or 75–85 °C ≥30 min with normal vib/acoustic.
Severity policy:
- Start with the max of per-signal severities; if ≥2 signals are abnormal, escalate one level (cap at CRITICAL).
- Ignore very brief blips (<2 samples just over a boundary) unless strongly pattern-matched.
- Be conservative; use input units.
```
**User prompt**
```
Analyze telemetry from a "${deviceType}" named "${deviceName}".
Data:
$[*]
```
3. **Response format** (optional)
In the previous step, we described desired response format in the system prompt, but it is possible to enforce format with JSON Schema if model you are using supports it. We recommend using JSON Schema if possible. Here is the example of response schema you can use (if using JSON Schema, in the system prompt just say that model output should be in JSON format):
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Alarm",
"type": "object",
"additionalProperties": false,
"required": ["type", "severity", "details"],
"properties": {
"type": {
"type": "string",
"description": "Incident type",
"enum": ["Bearing Wear", "Misalignment", "Imbalance", "Overheating"]
},
"severity": {
"type": "string",
"description": "Severity level of the incident",
"enum": ["WARNING", "MINOR", "MAJOR", "CRITICAL"]
},
"details": {
"type": "object",
"additionalProperties": false,
"required": ["summary"],
"properties": {
"summary": {
"type": "string",
"description": "2–3 sentences in plain English of concise, plain-language summary for maintenance teams; include units when citing values."
}
}
}
}
}
```
4. **How it works**
When the message containing sample data from **Pump-103** is processed, the templates are substituted:
* `${deviceName}` → `"Pump-103"`
* `${deviceType}` → `"CentrifugalPump"`
* `$[*]` → the entire message payload JSON (e.g. telemetry data we collected in step 1)
> **Tip:** `${*}` can substitute the entire metadata JSON if needed.
The final instruction sent to the model is the System prompt plus the substituted User prompt. AI response will be placed in outgoing message payload.
5. **Expected AI output**
Given the sample data from step 1, the AI will likely output something like this:
```json
{
"type": "Bearing Wear",
"severity": "CRITICAL",
"details": {
"summary": "Pump-103 showed a vibration spike from 4.2 to 8.0 mm/s with continued elevated levels (6.2–7.2 mm/s), along with a temperature spike to 86 °C and recurrent 84 °C. With acoustics only in the WARNING band (~17–18%), this pattern indicates bearing wear; inspect and replace the bearing promptly to prevent failure."
}
}
```
6. **Next steps**
Check the AI response: if it’s a non-empty object, it’s ready-to-use alarm JSON. Route it directly to the "create alarm" node to create an alarm. If it is empty, just ignore the output as everything is normal.
#### Example Usage: AI-Powered Alarm Analysis #### Example Usage: AI-Powered Alarm Analysis
This example demonstrates how to use the AI node to automatically analyze a new device alarm, generate a human-readable summary, and suggest troubleshooting steps. This example demonstrates how to use the AI node to automatically analyze a new device alarm, generate a human-readable summary, and suggest troubleshooting steps.

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

@ -6868,7 +6868,23 @@
"scan-qr-code": "Scan QR Code", "scan-qr-code": "Scan QR Code",
"make-phone-call": "Make phone call", "make-phone-call": "Make phone call",
"get-location": "Get phone location", "get-location": "Get phone location",
"take-screenshot": "Take screenshot" "take-screenshot": "Take screenshot",
"handle-provision-success-function": "Handle provision success function",
"get-location-function": "Get location function",
"process-launch-result-function": "Process launch result function",
"get-phone-number-function": "Get phone number function",
"process-image-function": "Process image function",
"process-qr-code-function": "Process QR code function",
"process-location-function": "Process location function",
"handle-empty-result-function": "Handle empty result function",
"handle-error-function": "Handle error function",
"handle-non-mobile-fallback-function": "Handle Non-Mobile fallback function",
"save-to-gallery": "Save to gallery",
"provision-type": "Provision type",
"auto": "Auto",
"wi-fi": "Wi-Fi",
"ble": "BLE",
"soft-ap": "Soft AP"
}, },
"custom-action-function": "Custom action function", "custom-action-function": "Custom action function",
"custom-pretty-function": "Custom action (with HTML template) function", "custom-pretty-function": "Custom action (with HTML template) function",

Loading…
Cancel
Save