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": [],
"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",
"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": "",
"dataKeySettingsSchema": "{}\n",
"settingsDirective": "tb-rpc-terminal-widget-settings",
@ -43,4 +43,4 @@
"public": true
}
]
}
}

2
msa/js-executor/docker/Dockerfile

@ -14,7 +14,7 @@
# 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 DOCKER_MODE true

22
msa/js-executor/package.json

@ -6,20 +6,20 @@
"main": "server.ts",
"bin": "server.js",
"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",
"start": "nodemon --watch '.' --ext 'ts' --exec 'ts-node server.ts'",
"start-prod": "nodemon --watch '.' --ext 'ts' --exec 'NODE_ENV=production ts-node server.ts'",
"build": "tsc"
},
"dependencies": {
"config": "^3.3.12",
"express": "^4.21.1",
"config": "^4.1.1",
"express": "^5.1.0",
"js-yaml": "^4.1.0",
"kafkajs": "^2.2.4",
"long": "^5.2.3",
"long": "^5.3.2",
"uuid-parse": "^1.1.0",
"winston": "^3.16.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"nyc": {
@ -32,14 +32,14 @@
},
"devDependencies": {
"@types/config": "^3.3.5",
"@types/express": "~4.17.21",
"@types/node": "~20.17.6",
"@types/express": "~5.0.3",
"@types/node": "~22.17.2",
"@types/uuid-parse": "^1.0.2",
"fs-extra": "^11.2.0",
"nodemon": "^3.1.7",
"pkg": "^5.8.1",
"@yao-pkg/pkg": "^6.6.0",
"fs-extra": "^11.3.1",
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",
"typescript": "5.5.4"
"typescript": "5.9.2"
},
"pkg": {
"assets": [

2
msa/js-executor/pom.xml

@ -71,7 +71,7 @@
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v20.18.0</nodeVersion>
<nodeVersion>v22.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</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.
#
FROM thingsboard/node:20.18.0-bookworm-slim
FROM thingsboard/node:22.18.0-bookworm-slim
ENV NODE_ENV production
ENV DOCKER_MODE true

28
msa/web-ui/package.json

@ -6,21 +6,21 @@
"main": "server.ts",
"bin": "server.js",
"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",
"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'",
"build": "tsc"
},
"dependencies": {
"compression": "^1.7.5",
"compression": "^1.8.1",
"config": "^3.3.12",
"connect-history-api-fallback": "^1.6.0",
"express": "^4.21.1",
"connect-history-api-fallback": "1.6.0",
"express": "^5.1.0",
"http": "0.0.0",
"http-proxy": "^1.18.1",
"js-yaml": "^4.1.0",
"winston": "^3.16.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"nyc": {
@ -32,17 +32,17 @@
]
},
"devDependencies": {
"@types/compression": "^1.7.5",
"@types/compression": "^1.8.1",
"@types/config": "^3.3.5",
"@types/connect-history-api-fallback": "^1.5.4",
"@types/express": "~4.17.21",
"@types/http-proxy": "^1.17.15",
"@types/node": "~20.17.6",
"fs-extra": "^11.2.0",
"nodemon": "^3.1.7",
"pkg": "^5.8.1",
"@types/connect-history-api-fallback": "1.5.4",
"@types/express": "~5.0.3",
"@types/http-proxy": "^1.17.16",
"@types/node": "~22.17.2",
"@yao-pkg/pkg": "^6.6.0",
"fs-extra": "^11.3.1",
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",
"typescript": "5.5.4"
"typescript": "5.9.2"
},
"pkg": {
"assets": [

2
msa/web-ui/pom.xml

@ -80,7 +80,7 @@
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v20.18.0</nodeVersion>
<nodeVersion>v22.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</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>
<snappy.version>1.1.10.5</snappy.version>
<rocksdbjni.version>9.10.0</rocksdbjni.version>
<netty.version>4.1.124.Final</netty.version>
</properties>
<modules>
@ -895,6 +896,13 @@
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-bom</artifactId>
<version>${netty.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
@ -909,7 +917,6 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>netty-mqtt</artifactId>

2
ui-ngx/pom.xml

@ -56,7 +56,7 @@
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v20.18.0</nodeVersion>
<nodeVersion>v22.18.0</nodeVersion>
<yarnVersion>v1.22.22</yarnVersion>
</configuration>
</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 {
Datasource,
datasourcesHasAggregation,
datasourcesHasOnlyComparisonAggregation,
DatasourceType,
defaultLegendConfig,
@ -49,7 +50,8 @@ import {
WidgetConfigMode,
WidgetSize,
widgetType,
WidgetTypeDescriptor
WidgetTypeDescriptor,
widgetTypeHasTimewindow
} from '@app/shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models';
@ -234,6 +236,7 @@ export class DashboardUtilsService {
public validateAndUpdateWidget(widget: Widget): Widget {
widget.config = this.validateAndUpdateWidgetConfig(widget.config, widget.type);
widget = this.validateAndUpdateWidgetTypeFqn(widget);
this.removeTimewindowConfigIfUnused(widget);
if (isDefined((widget as any).title)) {
delete (widget as any).title;
}
@ -294,8 +297,11 @@ export class DashboardUtilsService {
}
widgetConfig.datasources = this.validateAndUpdateDatasources(widgetConfig.datasources);
if (type === widgetType.latest) {
const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources);
widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService);
if (datasourcesHasAggregation(widgetConfig.datasources)) {
const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources);
widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true,
onlyHistoryTimewindow, this.timeService, false);
}
} else if (type === widgetType.rpc) {
if (widgetConfig.targetDeviceAliasIds && widgetConfig.targetDeviceAliasIds.length) {
widgetConfig.targetDevice = {
@ -348,6 +354,33 @@ export class DashboardUtilsService {
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 {
const config = widget.config;
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 {
CompiledTbFunction,
compileTbFunction, GenericFunction,
compileTbFunction,
GenericFunction,
isNotEmptyTbFunction,
TbFunction
} 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 {
const json = JSON.stringify(obj);
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);
}
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 {
if (isUndefined(length) || length == null) {
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 {
AIModel?: AiModel;
isAdd?: boolean;
name?: string;
}
@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(
takeUntilDestroyed()
).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) {
this.dashboardUtils.prepareWidgetForSaving(widget);
if (this.addingLayoutCtx) {
this.addWidgetToLayout(widget, this.addingLayoutCtx.id);
this.addingLayoutCtx = null;
@ -1410,7 +1411,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
saveWidget() {
this.editWidgetComponent.widgetFormGroup.markAsPristine();
const widget = deepClone(this.editingWidget);
const widget = this.dashboardUtils.prepareWidgetForSaving(deepClone(this.editingWidget));
const widgetLayout = deepClone(this.editingWidgetLayout);
const id = this.editingWidgetOriginal.id;
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"
(entityChanged)="onEntityChange($event)"
[entityType]="entityType.AI_MODEL"
(createNew)="createModelAi('modelId')"
(createNew)="createModelAi($event, 'modelId')"
formControlName="modelId">
</tb-entity-autocomplete>
</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) {
delete configuration.systemPrompt;
delete config.systemPrompt;
}
return deepTrim(configuration);
return deepTrim(config);
}
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}`);
}
createModelAi(formControl: string) {
createModelAi(name: string, formControl: string) {
this.dialog.open<AIModelDialogComponent, AIModelDialogData, AiModel>(AIModelDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
isAdd: true
isAdd: true,
name
}
}).afterClosed()
.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 { AppState } from '@core/core.state';
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 { isDefinedAndNotNull } from '@core/utils';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { IAliasController } from '@core/api/widget-api.models';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models';
export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks;
@ -107,6 +115,11 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement
if (this.isAdd) {
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.updateValidators(false);
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 class="flex-1"></span>
<button *ngIf="allowAcknowledgment"
mat-icon-button [disabled]="isLoading$ | async"
mat-icon-button
matTooltip="{{ 'alarm.acknowledge' | translate }}"
matTooltipPosition="above"
(click)="ackAlarms($event)">
<mat-icon>done</mat-icon>
</button>
<button *ngIf="ctx.settings.allowClear" mat-icon-button
[disabled]="isLoading$ | async"
<button *ngIf="ctx.settings.allowClear"
mat-icon-button
matTooltip="{{ 'alarm.clear' | translate }}"
matTooltipPosition="above"
(click)="clearAlarms($event)">
@ -107,7 +107,7 @@
</span>
</ng-template>
<button *ngIf="allowAssign"
mat-icon-button [disabled]="isLoading$ | async"
mat-icon-button
matTooltip="{{ 'alarm.assign' | translate }}"
matTooltipPosition="above"
(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);
}
if (isDefined(content)) {
if (typeof content === 'object') {
content = JSON.stringify(content);
}
content = this.utils.customTranslation(content, content);
switch (typeof content) {
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'">
<ng-container *ngFor="let actionDescriptor of entity.actionCellButtons; trackBy: trackByActionCellDescriptionId">
<span *ngIf="!actionDescriptor.icon" style="width: 48px;"></span>
<button mat-icon-button [disabled]="isLoading$ | async"
<button mat-icon-button
*ngIf="actionDescriptor.icon"
matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above"
@ -83,7 +83,6 @@
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<ng-container *ngFor="let actionDescriptor of entity.actionCellButtons; trackBy: trackByActionCellDescriptionId">
<button mat-menu-item *ngIf="actionDescriptor.icon"
[disabled]="isLoading$ | async"
(click)="onActionButtonClick($event, entity, actionDescriptor)">
<tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
<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', () => {
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-form-field>
</div>
<ng-container [formGroup]="mobileActionTypeFormGroup" [ngSwitch]="mobileActionFormGroup.get('type').value">
<ng-template [ngSwitchCase]="mobileActionType.deviceProvision">
<tb-js-func
formControlName="handleProvisionSuccessFunction"
functionName="handleProvisionSuccess"
withModules
[functionArgs]="['deviceName', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.mapDirection ||
mobileActionFormGroup.get('type').value === mobileActionType.mapLocation ?
mobileActionFormGroup.get('type').value : ''">
<tb-js-func
formControlName="getLocationFunction"
functionName="getLocation"
withModules
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_get_location_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionType.makePhoneCall">
<tb-js-func
formControlName="getPhoneNumberFunction"
functionName="getPhoneNumber"
withModules
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_get_phone_number_fn"
></tb-js-func>
</ng-template>
<ng-template [ngSwitchCase]="mobileActionFormGroup.get('type').value === mobileActionType.takePhoto ||
mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery ||
mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot ?
mobileActionFormGroup.get('type').value : ''">
<tb-js-func
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 [formGroup]="mobileActionTypeFormGroup">
@if (mobileActionFormGroup.get('type').value === mobileActionType.takePhoto ||
mobileActionFormGroup.get('type').value === mobileActionType.takePictureFromGallery ||
mobileActionFormGroup.get('type').value === mobileActionType.takeScreenshot) {
<div class="tb-form-row">
<mat-slide-toggle class="mat-slide" formControlName="saveToGallery">
{{ 'widget-action.mobile.save-to-gallery' | translate }}
</mat-slide-toggle>
</div>
}
@if (mobileActionFormGroup.get('type').value === mobileActionType.deviceProvision) {
<div class="tb-form-row">
<div class="fixed-title-width">{{ 'widget-action.mobile.provision-type' | translate }}*</div>
<mat-form-field class="flex-1" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="provisionType">
<mat-option *ngFor="let type of provisionTypes" [value]="type">
{{ provisionTypeTranslationMap.get(type) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
}
@for (config of actionConfig; track config.formControlName) {
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description class="flex items-stretch justify-start">
{{ config.title | translate }}
</mat-panel-description>
</mat-expansion-panel-header>
<tb-js-func
[formControlName]="config.formControlName"
[functionName]="config.functionName"
withModules
[functionArgs]="config.functionArgs"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
[helpId]="config.helpId"
></tb-js-func>
</mat-expansion-panel>
</div>
}
</ng-container>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleEmptyResultFunction"
functionName="handleEmptyResult"
withModules
[functionArgs]="['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_handle_empty_result_fn"
></tb-js-func>
<tb-js-func *ngIf="mobileActionFormGroup.get('type').value"
formControlName="handleErrorFunction"
functionName="handleError"
withModules
[functionArgs]="['error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel']"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
helpId="widget/action/mobile_handle_error_fn"
></tb-js-func>
@if(mobileActionFormGroup.get('type').value) {
@for (config of commonActionConfig; track config.formControlName) {
<div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-description class="flex items-stretch justify-start">
{{ config.title | translate }}
</mat-panel-description>
</mat-expansion-panel-header>
<tb-js-func
[formControlName]="config.formControlName"
[functionName]="config.functionName"
withModules
[functionArgs]="config.functionArgs"
[globalVariables]="functionScopeVariables"
[editorCompleter]="customActionEditorCompleter"
hideBrackets
[helpId]="config.helpId"
></tb-js-func>
</mat-expansion-panel>
</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';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
ActionConfig,
ProvisionType,
provisionTypeTranslationMap,
WidgetActionType,
WidgetMobileActionDescriptor,
WidgetMobileActionType,
@ -35,6 +38,7 @@ import {
getDefaultGetPhoneNumberFunction,
getDefaultHandleEmptyResultFunction,
getDefaultHandleErrorFunction,
getDefaultHandleNonMobileFallBackFunction,
getDefaultProcessImageFunction,
getDefaultProcessLaunchResultFunction,
getDefaultProcessLocationFunction,
@ -68,6 +72,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
functionScopeVariables: string[];
actionConfig: ActionConfig[];
commonActionConfig: ActionConfig[];
provisionTypes: string[] = Object.keys(ProvisionType);
provisionTypeTranslationMap = provisionTypeTranslationMap;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
@ -99,8 +109,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
this.mobileActionFormGroup = this.fb.group({
type: [null, Validators.required],
handleEmptyResultFunction: [null],
handleErrorFunction: [null]
handleErrorFunction: [null],
handleNonMobileFallbackFunction: [null]
});
this.getCommonActionConfigs();
this.mobileActionFormGroup.get('type').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe((type: WidgetMobileActionType) => {
@ -109,6 +121,7 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
action = {...action, ...this.mobileActionTypeFormGroup.value};
}
this.updateMobileActionType(type, action);
this.getActionConfigs();
});
this.mobileActionFormGroup.valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
@ -133,10 +146,14 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
}
writeValue(value: WidgetMobileActionDescriptor | null): void {
this.mobileActionFormGroup.patchValue({type: value?.type,
handleEmptyResultFunction: value?.handleEmptyResultFunction,
handleErrorFunction: value?.handleErrorFunction}, {emitEvent: false});
this.mobileActionFormGroup.patchValue({
type: value?.type,
handleEmptyResultFunction: value?.handleEmptyResultFunction,
handleErrorFunction: value?.handleErrorFunction,
handleNonMobileFallbackFunction: value?.handleNonMobileFallbackFunction
}, {emitEvent: false});
this.updateMobileActionType(value?.type, value);
this.getActionConfigs();
}
private updateModel() {
@ -164,6 +181,12 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
handleErrorFunction = getDefaultHandleErrorFunction(type);
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({});
if (type) {
@ -183,6 +206,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
'processImageFunction',
this.fb.control(processImageFunction, [])
);
this.mobileActionTypeFormGroup.addControl(
'saveToGallery',
this.fb.control(action?.saveToGallery || false, [])
);
break;
case WidgetMobileActionType.mapDirection:
case WidgetMobileActionType.mapLocation:
@ -267,6 +294,10 @@ export class MobileActionEditorComponent implements ControlValueAccessor, OnInit
'handleProvisionSuccessFunction',
this.fb.control(handleProvisionSuccessFunction, [Validators.required])
);
this.mobileActionTypeFormGroup.addControl(
'provisionType',
this.fb.control(action?.provisionType || ProvisionType.auto, [])
);
}
}
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;
}

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' +
'}\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 =
'// 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' +
@ -326,3 +334,5 @@ export const getDefaultHandleErrorFunction = (type: WidgetMobileActionType): TbF
}
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>
<button *ngIf="actionDescriptor.icon"
class="tb-mat-40"
mat-icon-button [disabled]="isLoading$ | async"
mat-icon-button
matTooltip="{{ actionDescriptor.displayName }}"
matTooltipPosition="above"
(click)="onActionButtonClick($event, row, actionDescriptor)">
@ -96,7 +96,6 @@
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<ng-container *ngFor="let actionDescriptor of row.actionCellButtons; trackBy: trackByActionCellDescriptionId">
<button mat-menu-item *ngIf="actionDescriptor.icon"
[disabled]="isLoading$ | async"
(click)="onActionButtonClick($event, row, actionDescriptor)">
<tb-icon matMenuItemIcon>{{actionDescriptor.icon}}</tb-icon>
<span>{{ actionDescriptor.displayName }}</span>

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

@ -38,6 +38,7 @@ import {
TargetDevice,
targetDeviceValid,
Widget,
widgetTypeCanHaveTimewindow,
WidgetConfigMode,
widgetType
} from '@shared/models/widget.models';
@ -53,7 +54,7 @@ import {
Validators
} from '@angular/forms';
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 { IAliasController } from '@core/api/widget-api.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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
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;
@Component({
@ -201,6 +204,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
constructor(protected store: Store<AppState>,
private utils: UtilsService,
private entityService: EntityService,
public timeService: TimeService,
private dialog: MatDialog,
public translate: TranslateService,
private fb: UntypedFormBuilder,
@ -366,16 +370,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
this.dataSettings = this.fb.group({});
this.targetDeviceSettings = 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({
useDashboardTimewindow: true,
displayTimewindow: true,
timewindow: 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.widgetType !== widgetType.rpc &&
@ -529,14 +533,17 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe
},
{emitEvent: false}
);
if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) {
if (widgetTypeCanHaveTimewindow(this.widgetType)) {
const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true;
this.dataSettings.get('timewindowConfig').patchValue({
useDashboardTimewindow,
displayTimewindow: isDefined(config.displayTimewindow) ?
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
}, {emitEvent: false});
}

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

@ -38,6 +38,7 @@ import {
} from '@angular/core';
import { DashboardWidget } from '@home/models/dashboard-component.models';
import {
MobileImageResult,
Widget,
WidgetAction,
WidgetActionDescriptor,
@ -126,6 +127,7 @@ import { IModulesMap } from '@modules/common/modules-map.models';
import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
import { CompiledTbFunction, compileTbFunction, isNotEmptyTbFunction } from '@shared/models/js-function.models';
import { HttpClient } from '@angular/common/http';
import { addDiagnosticChain } from '@angular/compiler-cli/src/ngtsc/diagnostics';
@Component({
selector: 'tb-widget',
@ -1222,12 +1224,16 @@ export class WidgetComponent extends PageComponent implements OnInit, OnChanges,
switch (type) {
case WidgetMobileActionType.takePictureFromGallery:
case WidgetMobileActionType.takePhoto:
case WidgetMobileActionType.takeScreenshot:
argsObservable = of([mobileAction.saveToGallery]);
break;
case WidgetMobileActionType.scanQrCode:
case WidgetMobileActionType.getLocation:
case WidgetMobileActionType.takeScreenshot:
case WidgetMobileActionType.deviceProvision:
argsObservable = of([]);
break;
case WidgetMobileActionType.deviceProvision:
argsObservable = of([mobileAction.provisionType]);
break;
case WidgetMobileActionType.mapDirection:
case WidgetMobileActionType.mapLocation:
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.takeScreenshot:
const imageUrl = actionResult.imageUrl;
if (!additionalParams) {
additionalParams = {};
}
additionalParams.imageInfo = actionResult.imageInfo;
if (isNotEmptyTbFunction(mobileAction.processImageFunction)) {
compileTbFunction(this.http, mobileAction.processImageFunction, 'imageUrl', '$event', 'widgetContext', 'entityId',
'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 {
platformType: PlatformType;
name?: string
}
@Component({
@ -55,6 +56,9 @@ export class MobileAppDialogComponent extends DialogComponent<MobileAppDialogCom
ngAfterViewInit(): void {
setTimeout(() => {
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.get('platformType').disable({emitEvent: false});
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"
[entityType]="entityType.MOBILE_APP"
[entitySubtype]="platformType.ANDROID"
(createNew)="createApplication('androidAppId', platformType.ANDROID)"
(createNew)="createApplication($event, 'androidAppId', platformType.ANDROID)"
formControlName="androidAppId">
</tb-entity-autocomplete>
<tb-entity-autocomplete
@ -68,7 +68,7 @@
labelText="mobile.ios-application"
[entityType]="entityType.MOBILE_APP"
[entitySubtype]="platformType.IOS"
(createNew)="createApplication('iosAppId', platformType.IOS)"
(createNew)="createApplication($event, 'iosAppId', platformType.IOS)"
formControlName="iosAppId">
</tb-entity-autocomplete>
<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, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
platformType
platformType,
name
}
}).afterClosed()
.subscribe((app) => {

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

@ -71,6 +71,11 @@
<span>
{{ noEntitiesMatchingText | translate: {entity: searchText} }}
</span>
@if (allowCreateNew) {
<span>
<a translate (click)="createNewEntity($event, searchText)">entity.create-new-key</a>
</span>
}
</ng-template>
</div>
</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>>();
@Output()
createNew = new EventEmitter<void>();
createNew = new EventEmitter<string>();
@ViewChild('entityInput', {static: true}) entityInput: ElementRef;
@ -451,9 +451,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
return entityType;
}
createNewEntity($event: Event) {
createNewEntity($event: Event, searchText?: string) {
$event.stopPropagation();
this.createNew.emit();
this.createNew.emit(searchText);
}
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 { FormBuilder, FormGroup, Validators } from '@angular/forms';
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 { TranslateService } from '@ngx-translate/core';
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({
selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME],
realtime: this.fb.group({
realtimeType: [ isDefined(realtime?.realtimeType) ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ],
timewindowMs: [ isDefined(realtime?.timewindowMs) ? this.timewindow.realtime.timewindowMs : null ],
interval: [ isDefined(realtime?.interval) ? this.timewindow.realtime.interval : null ],
quickInterval: [ isDefined(realtime?.quickInterval) ? this.timewindow.realtime.quickInterval : null ],
disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomInterval)
? this.timewindow.realtime?.disableCustomInterval : false ],
disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomGroupInterval)
? this.timewindow.realtime?.disableCustomGroupInterval : false ],
hideInterval: [ isDefinedAndNotNull(this.timewindow.realtime.hideInterval)
? this.timewindow.realtime.hideInterval : false ],
realtimeType: [ isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ],
timewindowMs: [ isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null ],
interval: [ isDefined(realtime?.interval) ? realtime.interval : null ],
quickInterval: [ isDefined(realtime?.quickInterval) ? realtime.quickInterval : null ],
disableCustomInterval: [ isDefinedAndNotNull(realtime?.disableCustomInterval)
? realtime.disableCustomInterval : false ],
disableCustomGroupInterval: [ isDefinedAndNotNull(realtime?.disableCustomGroupInterval)
? realtime.disableCustomGroupInterval : false ],
hideInterval: [ isDefinedAndNotNull(realtime?.hideInterval)
? realtime.hideInterval : false ],
hideLastInterval: [{
value: isDefinedAndNotNull(this.timewindow.realtime.hideLastInterval)
? this.timewindow.realtime.hideLastInterval : false,
disabled: this.timewindow.realtime.hideInterval
value: isDefinedAndNotNull(realtime?.hideLastInterval)
? realtime.hideLastInterval : false,
disabled: realtime?.hideInterval
}],
hideQuickInterval: [{
value: isDefinedAndNotNull(this.timewindow.realtime.hideQuickInterval)
? this.timewindow.realtime.hideQuickInterval : false,
disabled: this.timewindow.realtime.hideInterval
value: isDefinedAndNotNull(realtime?.hideQuickInterval)
? realtime.hideQuickInterval : false,
disabled: realtime?.hideInterval
}],
advancedParams: this.fb.group({
allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedLastIntervals)
? this.timewindow.realtime.advancedParams.allowedLastIntervals : null ],
allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedQuickIntervals)
? this.timewindow.realtime.advancedParams.allowedQuickIntervals : null ],
lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.lastAggIntervalsConfig)
? this.timewindow.realtime.advancedParams.lastAggIntervalsConfig : null ],
quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.quickAggIntervalsConfig)
? this.timewindow.realtime.advancedParams.quickAggIntervalsConfig : null ]
allowedLastIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedLastIntervals)
? realtime.advancedParams.allowedLastIntervals : null ],
allowedQuickIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedQuickIntervals)
? realtime.advancedParams.allowedQuickIntervals : null ],
lastAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.lastAggIntervalsConfig)
? realtime.advancedParams.lastAggIntervalsConfig : null ],
quickAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.quickAggIntervalsConfig)
? realtime.advancedParams.quickAggIntervalsConfig : null ]
})
}),
history: this.fb.group({
historyType: [ isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL ],
timewindowMs: [ isDefined(history?.timewindowMs) ? this.timewindow.history.timewindowMs : null ],
interval: [ isDefined(history?.interval) ? this.timewindow.history.interval : null ],
fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? this.timewindow.history.fixedTimewindow : null ],
quickInterval: [ isDefined(history?.quickInterval) ? this.timewindow.history.quickInterval : null ],
disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomInterval)
? this.timewindow.history?.disableCustomInterval : false ],
disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomGroupInterval)
? this.timewindow.history?.disableCustomGroupInterval : false ],
hideInterval: [ isDefinedAndNotNull(this.timewindow.history.hideInterval)
? this.timewindow.history.hideInterval : false ],
historyType: [ isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL ],
timewindowMs: [ isDefined(history?.timewindowMs) ? history.timewindowMs : null ],
interval: [ isDefined(history?.interval) ? history.interval : null ],
fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? history.fixedTimewindow : null ],
quickInterval: [ isDefined(history?.quickInterval) ? history.quickInterval : null ],
disableCustomInterval: [ isDefinedAndNotNull(history?.disableCustomInterval)
? history.disableCustomInterval : false ],
disableCustomGroupInterval: [ isDefinedAndNotNull(history?.disableCustomGroupInterval)
? history.disableCustomGroupInterval : false ],
hideInterval: [ isDefinedAndNotNull(history?.hideInterval)
? history.hideInterval : false ],
hideLastInterval: [{
value: isDefinedAndNotNull(this.timewindow.history.hideLastInterval)
? this.timewindow.history.hideLastInterval : false,
disabled: this.timewindow.history.hideInterval
value: isDefinedAndNotNull(history?.hideLastInterval)
? history.hideLastInterval : false,
disabled: history?.hideInterval
}],
hideQuickInterval: [{
value: isDefinedAndNotNull(this.timewindow.history.hideQuickInterval)
? this.timewindow.history.hideQuickInterval : false,
disabled: this.timewindow.history.hideInterval
value: isDefinedAndNotNull(history?.hideQuickInterval)
? history.hideQuickInterval : false,
disabled: history?.hideInterval
}],
hideFixedInterval: [{
value: isDefinedAndNotNull(this.timewindow.history.hideFixedInterval)
? this.timewindow.history.hideFixedInterval : false,
disabled: this.timewindow.history.hideInterval
value: isDefinedAndNotNull(history?.hideFixedInterval)
? history.hideFixedInterval : false,
disabled: history?.hideInterval
}],
advancedParams: this.fb.group({
allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedLastIntervals)
? this.timewindow.history.advancedParams.allowedLastIntervals : null ],
allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedQuickIntervals)
? this.timewindow.history.advancedParams.allowedQuickIntervals : null ],
lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.lastAggIntervalsConfig)
? this.timewindow.history.advancedParams.lastAggIntervalsConfig : null ],
quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.quickAggIntervalsConfig)
? this.timewindow.history.advancedParams.quickAggIntervalsConfig : null ]
allowedLastIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedLastIntervals)
? history.advancedParams.allowedLastIntervals : null ],
allowedQuickIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedQuickIntervals)
? history.advancedParams.allowedQuickIntervals : null ],
lastAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.lastAggIntervalsConfig)
? history.advancedParams.lastAggIntervalsConfig : null ],
quickAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.quickAggIntervalsConfig)
? history.advancedParams.quickAggIntervalsConfig : null ]
})
}),
aggregation: this.fb.group({
@ -423,7 +431,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
update() {
const timewindowFormValue = this.timewindowForm.getRawValue();
this.timewindow = mergeDeep(this.timewindow, timewindowFormValue);
this.timewindow = mergeDeepIgnoreArray(this.timewindow, timewindowFormValue);
const realtimeConfigurableLastIntervalsAvailable = !(timewindowFormValue.hideAggInterval &&
(timewindowFormValue.realtime.hideInterval || timewindowFormValue.realtime.hideLastInterval));
@ -434,82 +442,50 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On
const historyConfigurableQuickIntervalsAvailable = !(timewindowFormValue.hideAggInterval &&
(timewindowFormValue.history.hideInterval || timewindowFormValue.history.hideQuickInterval));
if (realtimeConfigurableLastIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedLastIntervals?.length) {
this.timewindow.realtime.advancedParams.allowedLastIntervals = timewindowFormValue.realtime.advancedParams.allowedLastIntervals;
} else {
if (!realtimeConfigurableLastIntervalsAvailable) {
delete this.timewindow.realtime.advancedParams.allowedLastIntervals;
}
if (realtimeConfigurableQuickIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedQuickIntervals?.length) {
this.timewindow.realtime.advancedParams.allowedQuickIntervals = timewindowFormValue.realtime.advancedParams.allowedQuickIntervals;
} else {
if (!realtimeConfigurableQuickIntervalsAvailable) {
delete this.timewindow.realtime.advancedParams.allowedQuickIntervals;
}
if (realtimeConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig) &&
Object.keys(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig).length) {
if (realtimeConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig)) {
this.timewindow.realtime.advancedParams.lastAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig;
} else {
delete this.timewindow.realtime.advancedParams.lastAggIntervalsConfig;
}
if (realtimeConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig) &&
Object.keys(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig).length) {
if (realtimeConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig)) {
this.timewindow.realtime.advancedParams.quickAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig;
} else {
delete this.timewindow.realtime.advancedParams.quickAggIntervalsConfig;
}
if (historyConfigurableLastIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedLastIntervals?.length) {
this.timewindow.history.advancedParams.allowedLastIntervals = timewindowFormValue.history.advancedParams.allowedLastIntervals;
} else {
if (!historyConfigurableLastIntervalsAvailable) {
delete this.timewindow.history.advancedParams.allowedLastIntervals;
}
if (historyConfigurableQuickIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedQuickIntervals?.length) {
this.timewindow.history.advancedParams.allowedQuickIntervals = timewindowFormValue.history.advancedParams.allowedQuickIntervals;
} else {
if (!historyConfigurableQuickIntervalsAvailable) {
delete this.timewindow.history.advancedParams.allowedQuickIntervals;
}
if (historyConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig) &&
Object.keys(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig).length) {
if (historyConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig)) {
this.timewindow.history.advancedParams.lastAggIntervalsConfig = timewindowFormValue.history.advancedParams.lastAggIntervalsConfig;
} else {
delete this.timewindow.history.advancedParams.lastAggIntervalsConfig;
}
if (historyConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig) &&
Object.keys(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig).length) {
if (historyConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig)) {
this.timewindow.history.advancedParams.quickAggIntervalsConfig = timewindowFormValue.history.advancedParams.quickAggIntervalsConfig;
} else {
delete this.timewindow.history.advancedParams.quickAggIntervalsConfig;
}
if (!Object.keys(this.timewindow.realtime.advancedParams).length) {
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 {
if (timewindowFormValue.hideAggregation) {
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) {
delete this.timewindow.aggregation;
}
this.dialogRef.close(this.timewindow);
deleteFalseProperties(this.timewindow);
this.dialogRef.close(deepClean(this.timewindow));
}
cancel() {

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

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

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

@ -271,6 +271,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = {
keyName: 'assignee',
value: '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 { 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 * as momentTz from 'moment-timezone';
import { IntervalType } from '@shared/models/telemetry/telemetry.models';
@ -281,19 +281,12 @@ export const historyInterval = (timewindowMs: number): Timewindow => ({
export const defaultTimewindow = (timeService: TimeService): Timewindow => {
const currentTime = moment().valueOf();
return {
displayValue: '',
hideAggregation: false,
hideAggInterval: false,
hideTimezone: false,
selectedTab: TimewindowType.REALTIME,
realtime: {
realtimeType: RealtimeWindowType.LAST_INTERVAL,
interval: SECOND,
timewindowMs: MINUTE,
quickInterval: QuickTimeInterval.CURRENT_DAY,
hideInterval: false,
hideLastInterval: false,
hideQuickInterval: false
},
history: {
historyType: HistoryWindowType.LAST_INTERVAL,
@ -304,10 +297,6 @@ export const defaultTimewindow = (timeService: TimeService): Timewindow => {
endTimeMs: currentTime
},
quickInterval: QuickTimeInterval.CURRENT_DAY,
hideInterval: false,
hideLastInterval: false,
hideFixedInterval: false,
hideQuickInterval: false
},
aggregation: {
type: AggregationType.AVG,
@ -325,40 +314,47 @@ const getTimewindowType = (timewindow: Timewindow): TimewindowType => {
};
export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean,
historyOnly: boolean, timeService: TimeService): Timewindow => {
historyOnly: boolean, timeService: TimeService, hasAggregation: boolean): Timewindow => {
const model = defaultTimewindow(timeService);
if (value) {
if (value.allowedAggTypes?.length) {
model.allowedAggTypes = value.allowedAggTypes;
}
model.hideAggregation = value.hideAggregation;
model.hideAggInterval = value.hideAggInterval;
model.hideTimezone = value.hideTimezone;
if (value.hideAggregation) {
model.hideAggregation = value.hideAggregation;
}
if (value.hideAggInterval) {
model.hideAggInterval = value.hideAggInterval;
}
if (value.hideTimezone) {
model.hideTimezone = value.hideTimezone;
}
model.selectedTab = getTimewindowType(value);
// for backward compatibility
if (isDefinedAndNotNull((value as any).hideInterval)) {
if ((value as any).hideInterval) {
model.realtime.hideInterval = (value as any).hideInterval;
model.history.hideInterval = (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;
delete (value as any).hideLastInterval;
}
if (isDefinedAndNotNull((value as any).hideQuickInterval)) {
if ((value as any).hideQuickInterval) {
model.realtime.hideQuickInterval = (value as any).hideQuickInterval;
delete (value as any).hideQuickInterval;
}
if (isDefined(value.realtime)) {
if (isDefinedAndNotNull(value.realtime.hideInterval)) {
if (value.realtime.hideInterval) {
model.realtime.hideInterval = value.realtime.hideInterval;
}
if (isDefinedAndNotNull(value.realtime.hideLastInterval)) {
if (value.realtime.hideLastInterval) {
model.realtime.hideLastInterval = value.realtime.hideLastInterval;
}
if (isDefinedAndNotNull(value.realtime.hideQuickInterval)) {
if (value.realtime.hideQuickInterval) {
model.realtime.hideQuickInterval = value.realtime.hideQuickInterval;
}
if (value.realtime.disableCustomInterval) {
@ -392,16 +388,16 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
}
}
if (isDefined(value.history)) {
if (isDefinedAndNotNull(value.history.hideInterval)) {
if (value.history.hideInterval) {
model.history.hideInterval = value.history.hideInterval;
}
if (isDefinedAndNotNull(value.history.hideLastInterval)) {
if (value.history.hideLastInterval) {
model.history.hideLastInterval = value.history.hideLastInterval;
}
if (isDefinedAndNotNull(value.history.hideFixedInterval)) {
if (value.history.hideFixedInterval) {
model.history.hideFixedInterval = value.history.hideFixedInterval;
}
if (isDefinedAndNotNull(value.history.hideQuickInterval)) {
if (value.history.hideQuickInterval) {
model.history.hideQuickInterval = value.history.hideQuickInterval;
}
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.timezone = value.timezone;
if (value.timezone) {
model.timezone = value.timezone;
}
}
if (quickIntervalOnly) {
model.realtime.realtimeType = RealtimeWindowType.INTERVAL;
@ -458,7 +456,7 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO
if (historyOnly) {
model.selectedTab = TimewindowType.HISTORY;
}
return model;
return clearTimewindowConfig(model, quickIntervalOnly, historyOnly, hasAggregation);
};
export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number,
@ -1098,19 +1096,91 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => {
if (timewindow.allowedAggTypes?.length) {
cloned.allowedAggTypes = timewindow.allowedAggTypes;
}
cloned.hideAggregation = timewindow.hideAggregation || false;
cloned.hideAggInterval = timewindow.hideAggInterval || false;
cloned.hideTimezone = timewindow.hideTimezone || false;
if (timewindow.hideAggregation) {
cloned.hideAggregation = timewindow.hideAggregation;
}
if (timewindow.hideAggInterval) {
cloned.hideAggInterval = timewindow.hideAggInterval;
}
if (timewindow.hideTimezone) {
cloned.hideTimezone = timewindow.hideTimezone;
}
if (isDefined(timewindow.selectedTab)) {
cloned.selectedTab = timewindow.selectedTab;
}
cloned.realtime = deepClone(timewindow.realtime);
cloned.history = deepClone(timewindow.history);
cloned.aggregation = deepClone(timewindow.aggregation);
cloned.timezone = timewindow.timezone;
if (isDefined(timewindow.realtime)) {
cloned.realtime = deepClone(timewindow.realtime);
}
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;
};
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 {
name: string;
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 { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { AbstractControl, UntypedFormGroup } from '@angular/forms';
import { AbstractControl, UntypedFormGroup, ValidatorFn } from '@angular/forms';
import { Observable } from 'rxjs';
import { Dashboard } from '@shared/models/dashboard.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 { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { TbUnit } from '@shared/models/unit.models';
import { ImageResourceInfo } from '@shared/models/resource.models';
export enum widgetType {
timeseries = 'timeseries',
@ -489,6 +490,14 @@ export const targetDeviceValid = (targetDevice?: TargetDevice): boolean =>
((targetDevice.type === TargetDeviceType.device && !!targetDevice.deviceId) ||
(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 => {
if (datasources) {
const foundDatasource = datasources.find(datasource => {
@ -622,6 +631,30 @@ export enum WidgetMobileActionType {
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 {
marker = 'marker',
polygon = 'polygon',
@ -675,6 +708,7 @@ export interface MobileLaunchResult {
export interface MobileImageResult {
imageUrl: string;
imageInfo?: ImageResourceInfo;
}
export interface MobileQrCodeResult {
@ -706,10 +740,12 @@ export interface WidgetMobileActionResult<T extends MobileActionResult> {
export interface ProvisionSuccessDescriptor {
handleProvisionSuccessFunction: TbFunction;
provisionType?: string;
}
export interface ProcessImageDescriptor {
processImageFunction: TbFunction;
saveToGallery?: boolean;
}
export interface ProcessLaunchResultDescriptor {
@ -743,6 +779,7 @@ export interface WidgetMobileActionDescriptor extends WidgetMobileActionDescript
type: WidgetMobileActionType;
handleErrorFunction?: TbFunction;
handleEmptyResultFunction?: TbFunction;
handleNonMobileFallbackFunction?: TbFunction;
}
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 { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.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 {
additionalMapDataSourcesToDatasources,
BaseMapSettings,
CirclesDataLayerSettings,
MapDataLayerSettings,
MapDataLayerType,
MapDataSourceSettings,
mapDataSourceSettingsToDatasource,
MapType
MapType,
MarkersDataLayerSettings,
PolygonsDataLayerSettings
} from '@shared/models/widget/maps/map.models';
import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition';
@ -124,6 +129,27 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources));
}
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;
};
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;
updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: T): void;
datasources(widget: Widget): Datasource[];
hasTimewindow(widget: Widget): boolean;
}
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
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",
"make-phone-call": "Make phone call",
"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-pretty-function": "Custom action (with HTML template) function",

Loading…
Cancel
Save