Merge branch 'master' into fix-edge-zombie-consumer-cleanup

This commit is contained in:
ShadowBlades 2025-08-28 17:53:43 +08:00 committed by GitHub
commit 63966c5771
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1987 additions and 1777 deletions

View File

@ -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
}
]
}
}

View File

@ -18,6 +18,7 @@ package org.thingsboard.server.dao.sql.cf;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.thingsboard.server.common.data.id.CalculatedFieldId;
import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity;
@ -36,7 +37,10 @@ public interface CalculatedFieldRepository extends JpaRepository<CalculatedField
Page<CalculatedFieldEntity> findAllByTenantId(UUID tenantId, Pageable pageable);
Page<CalculatedFieldEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, Pageable pageable);
@Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " +
"AND cf.entityId = :entityId " +
"AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)")
Page<CalculatedFieldEntity> findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, String textSearch, Pageable pageable);
List<CalculatedFieldEntity> findAllByTenantId(UUID tenantId);

View File

@ -85,7 +85,7 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao<CalculatedFieldEntity,
@Override
public PageData<CalculatedField> findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) {
log.debug("Try to find calculated fields by entityId[{}] and pageLink [{}]", entityId, pageLink);
return DaoUtil.toPageData(calculatedFieldRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), DaoUtil.toPageable(pageLink)));
return DaoUtil.toPageData(calculatedFieldRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink)));
}
@Override

View File

@ -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

View File

@ -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": [

View File

@ -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>

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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": [

View File

@ -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>

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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) => {

View File

@ -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;

View File

@ -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>

View File

@ -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) => {

View File

@ -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()) {

View File

@ -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)">

View File

@ -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>

View File

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

View File

@ -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>

View File

@ -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;
}

View File

@ -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;

View File

@ -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>

View File

@ -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});
}

View File

@ -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);
}
}
);
}
}
}
);

View File

@ -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;

View File

@ -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">

View File

@ -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) => {

View File

@ -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>

View File

@ -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 {

View File

@ -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() {

View File

@ -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;

View File

@ -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(() => {

View File

@ -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};

View File

@ -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 {

View File

@ -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;
};

View File

@ -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[] = [

View File

@ -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",