Browse Source

Merge branch 'rc' into fix/3912-device-table

pull/11758/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
b9604fa4d0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      application/src/main/data/json/system/widget_types/gateway_connectors.json
  2. 24
      application/src/main/data/json/tenant/dashboards/gateways.json
  3. 2
      dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java
  4. 2
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts
  5. 9
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract.ts
  6. 22
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts
  7. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/opc-version-processor.abstract.ts
  8. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html
  9. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts
  10. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts
  11. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts
  12. 11
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html
  13. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html
  14. 1
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract.ts
  15. 17
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  16. 15
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract.ts
  17. 22
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.ts
  18. 19
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-legacy-basic-config.component.ts
  19. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html
  20. 23
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html
  21. 93
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  22. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  23. 2
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts
  24. 15
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts
  25. 74
      ui-ngx/src/assets/help/en_US/scada/symbol_state_render_fn.md
  26. 144
      ui-ngx/src/assets/help/en_US/scada/tag_click_action_fn.md
  27. 59
      ui-ngx/src/assets/help/en_US/scada/tag_state_render_fn.md
  28. 5
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  29. 7
      ui-ngx/src/assets/metadata/connector-default-configs/modbus.json

2
application/src/main/data/json/system/widget_types/gateway_connectors.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-gateway-connector [device]=\"entityId\" *ngIf=\"entityId\" [ctx]=\"ctx\"></tb-gateway-connector>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
"controllerScript": "self.onInit = function() {\n if (self.ctx.datasources && self.ctx.datasources.length) {\n self.ctx.$scope.entityId = self.ctx.datasources[0].entity.id;\n }\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.gatewayConnectors?.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n singleEntity: true\n };\n}",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{},\"title\":\"Gateway connectors\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"enableDataExport\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":500},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showLegend\":false}"

24
application/src/main/data/json/tenant/dashboards/gateways.json

@ -360,6 +360,30 @@
"color": "#2196f3",
"settings": {},
"_hash": 0.7454705362378311
},
{
"name": "lastConnectTime",
"type": "attribute",
"label": "lastConnectTime",
"color": "#4caf50",
"settings": {},
"_hash": 0.7249585632235194
},
{
"name": "lastDisconnectTime",
"type": "attribute",
"label": "lastDisconnectTime",
"color": "#f44336",
"settings": {},
"_hash": 0.9812430092707332
},
{
"name": "active",
"type": "attribute",
"label": "active",
"color": "#ffc107",
"settings": {},
"_hash": 0.9216097189544408
}
]
}

2
dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java

@ -638,7 +638,7 @@ public class BaseImageService extends BaseResourceService implements ImageServic
ImageCacheKey key = getKeyFromUrl(tenantId, url);
if (key != null) {
var imageInfo = getImageInfoByTenantIdAndKey(key.getTenantId(), key.getResourceKey());
if (imageInfo != null) {
if (imageInfo != null && !(TenantId.SYS_TENANT_ID.equals(imageInfo.getTenantId()) && ResourceSubType.SCADA_SYMBOL.equals(imageInfo.getResourceSubType()))) {
byte[] data = key.isPreview() ? getImagePreview(tenantId, imageInfo.getId()) : getImageData(tenantId, imageInfo.getId());
ImageDescriptor descriptor = getImageDescriptor(imageInfo, key.isPreview());
String tbImagePrefix = "";

2
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts

@ -609,7 +609,7 @@ export class TbTimeSeriesChart {
echartsModule.init();
this.renderer.setStyle(this.chartElement, 'letterSpacing', 'normal');
this.timeSeriesChart = echarts.init(this.chartElement, null, {
renderer: 'canvas'
renderer: 'svg'
});
this.timeSeriesChartOptions = {
darkMode: this.darkMode,

9
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-basic-config.abstract.ts

@ -14,16 +14,17 @@
/// limitations under the License.
///
import { Directive, inject, Input, OnDestroy, TemplateRef } from '@angular/core';
import { AfterViewInit, Directive, EventEmitter, inject, Input, OnDestroy, Output, TemplateRef } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, ValidationErrors, Validator } from '@angular/forms';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Directive()
export abstract class GatewayConnectorBasicConfigDirective<InputBasicConfig, OutputBasicConfig>
implements ControlValueAccessor, Validator, OnDestroy {
implements AfterViewInit, ControlValueAccessor, Validator, OnDestroy {
@Input() generalTabContent: TemplateRef<any>;
@Output() initialized = new EventEmitter<void>();
basicFormGroup: FormGroup;
@ -45,6 +46,10 @@ export abstract class GatewayConnectorBasicConfigDirective<InputBasicConfig, Out
this.destroy$.complete();
}
ngAfterViewInit(): void {
this.initialized.emit();
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : { basicFormGroup: { valid: false } };
}

22
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/gateway-connector-version-processor.abstract.ts

@ -23,14 +23,22 @@ export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
protected constructor(protected gatewayVersionIn: string | number, protected connector: GatewayConnector<BasicConfig>) {
this.gatewayVersion = this.parseVersion(this.gatewayVersionIn);
this.configVersion = this.parseVersion(connector.configVersion);
this.configVersion = this.parseVersion(this.connector.configVersion);
}
getProcessedByVersion(): GatewayConnector<BasicConfig> {
if (this.isVersionUpdateNeeded()) {
return this.isVersionUpgradeNeeded()
? this.getUpgradedVersion()
: this.getDowngradedVersion();
if (!this.isVersionUpdateNeeded()) {
return this.connector;
}
return this.processVersionUpdate();
}
private processVersionUpdate(): GatewayConnector<BasicConfig> {
if (this.isVersionUpgradeNeeded()) {
return this.getUpgradedVersion();
} else if (this.isVersionDowngradeNeeded()) {
return this.getDowngradedVersion();
}
return this.connector;
@ -48,6 +56,10 @@ export abstract class GatewayConnectorVersionProcessor<BasicConfig> {
return this.gatewayVersionIn === GatewayVersion.Current && (!this.configVersion || this.configVersion < this.gatewayVersion);
}
private isVersionDowngradeNeeded(): boolean {
return this.configVersion && this.connector.configVersion === GatewayVersion.Current && (this.configVersion > this.gatewayVersion);
}
private parseVersion(version: string | number): number {
if (isNumber(version)) {
return version as number;

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/abstract/opc-version-processor.abstract.ts

@ -38,7 +38,7 @@ export class OpcVersionProcessor extends GatewayConnectorVersionProcessor<OPCBas
...this.connector,
configurationJson: {
server: server ? OpcVersionMappingUtil.mapServerToUpgradedVersion(server) : {},
mapping: server.mapping ? OpcVersionMappingUtil.mapMappingToUpgradedVersion(server.mapping) : [],
mapping: server?.mapping ? OpcVersionMappingUtil.mapMappingToUpgradedVersion(server.mapping) : [],
},
configVersion: this.gatewayVersionIn
} as GatewayConnector<OPCBasicConfig_v3_5_2>;

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.html

@ -693,7 +693,7 @@
</mat-form-field>
<mat-form-field appearance="outline" class="flex">
<mat-label translate>gateway.inactivity-check-period-seconds</mat-label>
<input matInput formControlName="inactivityCheckPeriodSeconds"/>
<input matInput type="number" min="0" formControlName="inactivityCheckPeriodSeconds"/>
<mat-error
*ngIf="basicFormGroup.get('thingsboard.checkingDeviceActivity.inactivityCheckPeriodSeconds').hasError('required')">
{{ 'gateway.inactivity-check-period-seconds-required' | translate }}

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts

@ -502,14 +502,14 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
}
private addFileStorageValidators(group: FormGroup): void {
['data_folder_path', 'max_file_count', 'max_read_records_count', 'max_records_per_file'].forEach(field => {
['max_file_count', 'max_read_records_count', 'max_records_per_file'].forEach(field => {
group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get(field).updateValueAndValidity({ emitEvent: false });
});
}
private addSqliteStorageValidators(group: FormGroup): void {
['data_file_path', 'messages_ttl_check_in_hours', 'messages_ttl_in_days'].forEach(field => {
['messages_ttl_check_in_hours', 'messages_ttl_in_days'].forEach(field => {
group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get(field).updateValueAndValidity({ emitEvent: false });
});

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts

@ -242,7 +242,7 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
consoleHandler: {
class: 'logging.StreamHandler',
formatter: 'LogFormatter',
level: 'DEBUG',
level: 0,
stream: 'ext://sys.stdout'
},
databaseHandler: {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/models/gateway-configuration.models.ts

@ -128,7 +128,7 @@ interface LogFormatterConfig {
interface StreamHandlerConfig {
class: string;
formatter: string;
level: string;
level: string | number;
stream: string;
}

11
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-data-keys-panel/mapping-data-keys-panel.component.html

@ -26,10 +26,13 @@
<mat-expansion-panel class="tb-settings" [expanded]="last">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<div class="title-container" *ngIf="keysType !== MappingKeysType.RPC_METHODS">
{{ keyControl.get('key').value }}{{ '-' }}
</div>
<div class="title-container">{{ valueTitle(keyControl) }}</div>
<ng-container *ngIf="keysType !== MappingKeysType.RPC_METHODS">
<div tbTruncateWithTooltip class="title-container">
{{ keyControl.get('key').value }}
</div>
{{ '-' }}
</ng-container>
<div tbTruncateWithTooltip class="title-container">{{ valueTitle(keyControl) }}</div>
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>

20
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html

@ -62,17 +62,25 @@
</mat-toolbar>
<div class="table-container">
<table mat-table [dataSource]="dataSource">
<ng-container [matColumnDef]="'name'">
<ng-container [matColumnDef]="'info'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.name' | translate }}
{{ 'gateway.info' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
{{ slave['name'] }}
<div tbTruncateWithTooltip>{{ slave['host'] ?? slave['port'] }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'unitId'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.unit-id' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
<div tbTruncateWithTooltip>{{ slave['unitId'] }}</div>
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.client-communication-type' | translate }}
<div tbTruncateWithTooltip>{{ 'gateway.client-communication-type' | translate }}</div>
</mat-header-cell>
<mat-cell *matCellDef="let slave" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(slave['type']) }}
@ -113,8 +121,8 @@
</div>
</mat-cell>
</ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let slave; columns: ['name', 'type', 'actions']"></mat-row>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['info', 'unitId', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let slave; columns: ['info', 'unitId', 'type', 'actions']"></mat-row>
</table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new">

1
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.abstract.ts

@ -113,7 +113,6 @@ export abstract class ModbusSlaveDialogAbstract<Component, Config> extends Dialo
private initializeSlaveFormGroup(): void {
this.slaveConfigFormGroup = this.fb.group({
name: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [ModbusProtocolType.TCP],
host: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],

17
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html

@ -27,23 +27,6 @@
</button>
</mat-toolbar>
<div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width slave-name-label tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.name-required') | translate"
*ngIf="slaveConfigFormGroup.get('name').hasError('required') &&
slaveConfigFormGroup.get('name').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
</div>
<div class="stroked tb-form-panel">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-flex row space-between align-center no-gap fill-width">

15
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract.ts

@ -17,11 +17,14 @@
import { Directive } from '@angular/core';
import { FormGroup } from '@angular/forms';
import {
BrokerConfig,
MappingType,
MQTTBasicConfig, MQTTBasicConfig_v3_5_2,
MQTTBasicConfig,
MQTTBasicConfig_v3_5_2,
RequestMappingData,
RequestMappingValue,
RequestType
RequestType,
WorkersConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isObject } from '@core/utils';
import {
@ -73,6 +76,14 @@ export abstract class MqttBasicConfigDirective<BasicConfig>
});
}
protected getBrokerMappedValue(broker: BrokerConfig, workers: WorkersConfig): BrokerConfig {
return {
...broker,
maxNumberOfWorkers: workers.maxNumberOfWorkers ?? 100,
maxMessageNumberPerWorker: workers.maxMessageNumberPerWorker ?? 10,
};
}
writeValue(basicConfig: BasicConfig): void {
this.basicFormGroup.setValue(this.mapConfigToFormValue(basicConfig), { emitEvent: false });
}

22
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.component.ts

@ -26,7 +26,6 @@ import {
import {
MqttBasicConfigDirective
} from '@home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-basic-config.abstract';
import { isDefinedAndNotNull } from '@core/utils';
import { CommonModule } from '@angular/common';
import { SharedModule } from '@shared/shared.module';
import {
@ -85,19 +84,14 @@ export class MqttBasicConfigComponent extends MqttBasicConfigDirective<MQTTBasic
}
protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTBasicConfig_v3_5_2 {
let { broker, workers, mapping, requestsMapping } = basicConfig || {};
const { broker, workers, mapping, requestsMapping } = basicConfig || {};
if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) {
broker = {
...broker,
...workers,
};
}
if ((requestsMapping as RequestMappingData[])?.length) {
requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingValue[]);
}
return { broker, mapping, requestsMapping };
return {
broker: this.getBrokerMappedValue(broker, workers),
mapping,
requestsMapping: (requestsMapping as RequestMappingData[])?.length
? this.getRequestDataObject(requestsMapping as RequestMappingValue[])
: {} as Record<RequestType, RequestMappingValue[]>
};
}
}

19
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt/basic-config/mqtt-legacy-basic-config.component.ts

@ -102,23 +102,16 @@ export class MqttLegacyBasicConfigComponent extends MqttBasicConfigDirective<MQT
}
protected override getMappedValue(basicConfig: MQTTBasicConfig_v3_5_2): MQTTLegacyBasicConfig {
let { broker, workers, mapping, requestsMapping } = basicConfig || {};
const { broker, workers, mapping, requestsMapping } = basicConfig || {};
if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) {
broker = {
...broker,
...workers,
};
}
if ((requestsMapping as RequestMappingData[])?.length) {
requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingValue[]);
}
const updatedRequestMapping = (requestsMapping as RequestMappingData[])?.length
? this.getRequestDataObject(requestsMapping as RequestMappingValue[])
: {} as Record<RequestType, RequestMappingData[]>;
return {
broker,
broker: this.getBrokerMappedValue(broker, workers),
mapping: MqttVersionMappingUtil.mapMappingToDowngradedVersion(mapping),
...(MqttVersionMappingUtil.mapRequestsToDowngradedVersion(requestsMapping as Record<RequestType, RequestMappingData[]>))
...(MqttVersionMappingUtil.mapRequestsToDowngradedVersion(updatedRequestMapping as Record<RequestType, RequestMappingData[]>))
};
}
}

4
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component.html

@ -70,8 +70,8 @@
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="keyControl.get(keyControl.get('value').value).hasError('required')
&& keyControl.get(keyControl.get('value').value).touched"
*ngIf="keyControl.get(keyControl.get('type').value).hasError('required')
&& keyControl.get(keyControl.get('type').value).touched"
class="tb-error">
warning
</mat-icon>

23
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.html

@ -181,9 +181,14 @@
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
/>
<ng-template #legacy>
<tb-mqtt-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/>
<tb-mqtt-legacy-basic-config
(initialized)="basicConfigInitSubject.next()"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
/>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.OPCUA">
@ -191,9 +196,14 @@
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
/>
<ng-template #legacy>
<tb-opc-ua-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/>
<tb-opc-ua-legacy-basic-config
(initialized)="basicConfigInitSubject.next()"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
/>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="ConnectorType.MODBUS">
@ -201,9 +211,14 @@
*ngIf="connectorForm.get('configVersion').value === GatewayVersion.Current else legacy"
formControlName="basicConfig"
[generalTabContent]="generalTabContent"
(initialized)="basicConfigInitSubject.next()"
/>
<ng-template #legacy>
<tb-modbus-legacy-basic-config formControlName="basicConfig" [generalTabContent]="generalTabContent"/>
<tb-modbus-legacy-basic-config
formControlName="basicConfig"
(initialized)="basicConfigInitSubject.next()"
[generalTabContent]="generalTabContent"
/>
</ng-template>
</ng-container>
</ng-container>
@ -229,7 +244,7 @@
<button mat-raised-button color="primary"
type="button"
[disabled]="!connectorForm.dirty || connectorForm.invalid"
(click)="saveConnector()">
(click)="saveConnector(false)">
{{ 'action.save' | translate }}
</button>
</div>

93
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts

@ -31,7 +31,7 @@ import { EntityId } from '@shared/models/id/entity-id';
import { AttributeService } from '@core/http/attribute.service';
import { TranslateService } from '@ngx-translate/core';
import { forkJoin, Observable, of, Subject, Subscription } from 'rxjs';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { AttributeData, AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { PageComponent } from '@shared/components/page.component';
import { PageLink } from '@shared/models/page/page-link';
import { AttributeDatasource } from '@home/models/datasource/attribute-datasource';
@ -110,8 +110,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
activeConnectors: Array<string>;
mode: ConfigurationModes = this.ConnectorConfigurationModes.BASIC;
initialConnector: GatewayConnector;
basicConfigInitSubject = new Subject<void>();
private gatewayVersion: string;
private isGatewayActive: boolean;
private inactiveConnectors: Array<string>;
private attributeDataSource: AttributeDatasource;
private inactiveConnectorsDataSource: AttributeDatasource;
@ -124,7 +126,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => {
this.onDataUpdated();
this.onErrorsUpdated();
}),
onDataUpdateError: (_, e) => this.ctx.ngZone.run(() => {
this.onDataUpdateError(e);
@ -155,8 +157,10 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = this.getSortingDataAccessor();
this.ctx.$scope.gatewayConnectors = this;
this.loadConnectors();
this.loadGatewayState();
this.observeModeChange();
}
@ -166,9 +170,9 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
super.ngOnDestroy();
}
saveConnector(): void {
saveConnector(isNew = true): void {
const value = this.getConnectorData();
const scope = (!this.initialConnector || this.activeConnectors.includes(this.initialConnector.name))
const scope = (isNew || this.activeConnectors.includes(this.initialConnector.name))
? AttributeScope.SHARED_SCOPE
: AttributeScope.SERVER_SCOPE;
@ -275,7 +279,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
isConnectorSynced(attribute: GatewayAttributeData): boolean {
const connectorData = attribute.value;
if (!connectorData.ts || attribute.skipSync) {
if (!connectorData.ts || attribute.skipSync || !this.isGatewayActive) {
return false;
}
const clientIndex = this.activeData.findIndex(data => {
@ -479,7 +483,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
filter(Boolean),
)
.subscribe(value => {
this.initialConnector = null;
if (this.connectorForm.disabled) {
this.connectorForm.enable();
}
@ -487,9 +490,16 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
value.configurationJson = {} as ConnectorBaseConfig;
}
value.basicConfig = value.configurationJson;
this.updateConnector(value);
this.initialConnector = value;
this.connectorForm.patchValue(value, {emitEvent: false});
this.generate('basicConfig.broker.clientId');
setTimeout(() => this.saveConnector());
if (this.connectorForm.get('type').value === value.type || !this.allowBasicConfig.has(value.type)) {
this.saveConnector();
} else {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.saveConnector();
});
}
});
}
@ -590,6 +600,19 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
private loadGatewayState(): void {
this.attributeService.getEntityAttributes(this.device, AttributeScope.SERVER_SCOPE)
.pipe(takeUntil(this.destroy$))
.subscribe((attributes: AttributeData[]) => {
const active = attributes.find(data => data.key === 'active').value;
const lastDisconnectedTime = attributes.find(data => data.key === 'lastDisconnectTime')?.value;
const lastConnectedTime = attributes.find(data => data.key === 'lastConnectTime').value;
this.isGatewayActive = this.getGatewayStatus(active, lastConnectedTime, lastDisconnectedTime);
});
}
private parseConnectors(attribute: GatewayAttributeData[]): string[] {
const connectors = attribute?.[0]?.value || [];
return isString(connectors) ? JSON.parse(connectors) : connectors;
@ -598,7 +621,14 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private observeModeChange(): void {
this.connectorForm.get('mode').valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(() => this.connectorForm.get('mode').markAsPristine());
.subscribe((mode) => {
this.connectorForm.get('mode').markAsPristine();
if (mode === ConfigurationModes.BASIC) {
this.basicConfigInitSubject.pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector({...this.initialConnector, mode: ConfigurationModes.BASIC});
});
}
});
}
private observeAttributeChange(): void {
@ -664,10 +694,29 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
console.error(errorText);
}
private onErrorsUpdated(): void {
this.cd.detectChanges();
}
private onDataUpdated(): void {
const dataSources = this.ctx.defaultSubscription.data;
const active = dataSources.find(data => data.dataKey.name === 'active').data[0][1];
const lastDisconnectedTime = dataSources.find(data => data.dataKey.name === 'lastDisconnectTime').data[0][1];
const lastConnectedTime = dataSources.find(data => data.dataKey.name === 'lastConnectTime').data[0][1];
this.isGatewayActive = this.getGatewayStatus(active, lastConnectedTime, lastDisconnectedTime);
this.cd.detectChanges();
}
private getGatewayStatus(active: boolean, lastConnectedTime: number, lastDisconnectedTime: number): boolean {
if (!active) {
return false;
}
return !lastDisconnectedTime || lastConnectedTime > lastDisconnectedTime;
}
private generateSubscription(): void {
if (this.subscription) {
this.subscription.unsubscribe();
@ -760,13 +809,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
case ConnectorType.MQTT:
case ConnectorType.OPCUA:
case ConnectorType.MODBUS:
this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false});
this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false});
setTimeout(() => {
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
});
this.updateBasicConfigConnector(connector);
break;
default:
this.connectorForm.patchValue({...connector, mode: null});
@ -775,6 +818,24 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.createJsonConfigWatcher();
}
private updateBasicConfigConnector(connector: GatewayConnector): void {
this.connectorForm.get('mode').setValue(connector.mode || ConfigurationModes.BASIC, {emitEvent: false});
this.connectorForm.get('configVersion').setValue(connector.configVersion, {emitEvent: false});
if ((!connector.mode || connector.mode === ConfigurationModes.BASIC) && this.connectorForm.get('type').value !== connector.type) {
this.basicConfigInitSubject.asObservable().pipe(take(1)).subscribe(() => {
this.patchBasicConfigConnector(connector);
});
} else {
this.patchBasicConfigConnector(connector);
}
}
private patchBasicConfigConnector(connector: GatewayConnector): void {
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
}
private toggleReportStrategy(type: ConnectorType): void {
const reportStrategyControl = this.connectorForm.get('reportStrategy');
if (type === ConnectorType.MODBUS) {

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts

@ -711,7 +711,7 @@ export const HelpLinkByMappingTypeMap = new Map<MappingType, string>(
[
[MappingType.DATA, helpBaseUrl + '/docs/iot-gateway/config/mqtt/#section-mapping'],
[MappingType.OPCUA, helpBaseUrl + '/docs/iot-gateway/config/opc-ua/#section-mapping'],
[MappingType.REQUESTS, helpBaseUrl + '/docs/iot-gateway/config/mqtt/#section-mapping']
[MappingType.REQUESTS, helpBaseUrl + '/docs/iot-gateway/config/mqtt/#requests-mapping']
]
);

2
ui-ngx/src/app/modules/home/components/widget/lib/gateway/utils/opc-version-mapping.util.ts

@ -45,7 +45,7 @@ export class OpcVersionMappingUtil {
static mapServerToDowngradedVersion(config: OPCBasicConfig_v3_5_2): LegacyServerConfig {
const { mapping, server } = config;
const { enableSubscriptions, ...restServer } = server;
const { enableSubscriptions, ...restServer } = server ?? {} as ServerConfig;
return {
...restServer,
mapping: mapping ? this.mapMappingToDowngradedVersion(mapping) : [],

15
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts

@ -25,7 +25,9 @@ import { DeviceCredentials, DeviceProfileInfo, DeviceTransportType } from '@shar
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { forkJoin } from 'rxjs';
import { forkJoin, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { HttpStatusCode } from '@angular/common/http';
export interface DeviceCredentialsDialogData {
isReadOnly: boolean;
@ -102,7 +104,16 @@ export class DeviceCredentialsDialogComponent extends
this.submitted = true;
const deviceCredentialsValue = this.deviceCredentialsFormGroup.value.credential;
this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue};
this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe(
this.deviceService.saveDeviceCredentials(this.deviceCredentials)
.pipe(
catchError((err) => {
if (err.status === HttpStatusCode.Conflict) {
return this.deviceService.getDeviceCredentials(this.deviceCredentials.deviceId.id);
}
return throwError(() => err);
})
)
.subscribe(
(deviceCredentials) => {
this.dialogRef.close(deviceCredentials);
}

74
ui-ngx/src/assets/help/en_US/scada/symbol_state_render_fn.md

@ -20,12 +20,15 @@ A JavaScript function used to render SCADA symbol state.
##### Examples
**Handle volume buttons state and appearance**
This JavaScript snippet manages the enablement and appearance of SVG buttons used to control volume settings based on device connectivity and value thresholds.
The 'Up' button is disabled when the volume reaches its maximum allowable value (`maxValue`), and the 'Down' button is disabled when the volume is at its minimum allowable value (`minValue`).
The 'Up' button is disabled when the volume reaches its maximum allowable value `maxValue`, and the 'Down' button is disabled when the volume is at its minimum allowable value `minValue`.
Both buttons are disabled when the device is offline. The visual cues for enabled/disabled states are represented by color changes.
```javascript
<br>
```javascript
var levelUpButton = ctx.tags.levelUpButton; // Button for increasing volume
var levelDownButton = ctx.tags.levelDownButton; // Button for decreasing volume
var levelArrowUp = ctx.tags.levelArrowUp; // Visual arrow for 'Up' button
@ -61,3 +64,70 @@ if (levelDownEnabled) {
{:copy-code}
```
<br>
**Manage icon and label visibility and styling**
This JavaScript code snippet dynamically handles the visibility and styling of an icon and label within a SCADA symbol.
It checks the `showIcon` and `showLabel` properties from the context to determine whether to display the icon and/or label.
If the icon or label is shown, the script sets their respective properties like size, color, and position.
The script utilizes the <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolapi" target="_blank">ScadaSymbolApi</a> to update the visual appearance of these elements based on the context properties.
<br>
This approach ensures that both the icon and label elements are dynamically shown or hidden based on context properties, with appropriate styling applied.
<br>
```javascript
// Retrieve the first icon and label elements from the context tags
var iconTag = ctx.tags.icon[0];
var labelTag = ctx.tags.label[0];
// Check whether the icon should be displayed, based on the context property
var showIcon = ctx.properties.showIcon;
var showLabel = ctx.properties.label;
if (showIcon) {
// Show the icon if the 'showIcon' property is true
iconTag.show();
// Set icon attributes such as icon type, size, and color
var icon = ctx.properties.icon;
var iconSize = ctx.properties.iconSize;
var iconColor = ctx.properties.iconColor;
// Use the ScadaSymbolApi to apply the icon, size, and color to the iconTag
ctx.api.icon(iconTag, icon, iconSize, iconColor, true);
// If the label is not shown, adjust the icon's position
if (!showLabel) {
iconTag.transform({translateX: 83, translateY: 137});
}
} else {
// Hide the icon if 'showIcon' is false
iconTag.hide();
}
if (showLabel) {
// Show the label if the 'showLabel' property is true
var labelTextFont = ctx.properties.labelTextFont;
var labelTextColor = ctx.properties.labelTextColor;
// Apply font and color to the label using the ScadaSymbolApi
ctx.api.font(labelTag, labelTextFont, labelTextColor);
// Set the text content of the label
ctx.api.text(labelTag, ctx.properties.labelText);
// If the icon is not shown, adjust the label's position
if (!showIcon) {
labelTag.transform({translateX: 10});
}
// Show the label
labelTag.show();
} else {
// Hide the label if 'showLabel' is false
labelTag.hide();
}
{:copy-code}
```

144
ui-ngx/src/assets/help/en_US/scada/tag_click_action_fn.md

@ -21,7 +21,34 @@ A JavaScript function invoked when user clicks on SVG element with specific tag.
<div class="divider"></div>
##### Examples
##### Examples without setting values for callAction function
**Invoke widget click action**
<br>
This JavaScript snippet demonstrates triggering a <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide//ui/widget-actions/#action-types" target="_blank">widget action</a> using the <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolcontext" target="_blank">ScadaSymbolContext API</a> when the click event occurs. The widget action will be linked to the behaviorId 'click', which defines the action that will be executed upon the event.
The behavior of this action depends on the type of widget action configured in the ThingsBoard platform (e.g., navigating to a dashboard state, updating the current state, opening a URL, etc.).
<br>
```javascript
ctx.api.callAction(event, 'click'); // Trigger widget action 'click' on event
{:copy-code}
```
<br>
This action is executed automatically upon the 'click' event, making it useful for scenarios where the user interacts with a widget by clicking on it.
*Example Use Case*
- **Navigate to Dashboard State:** If the 'click' action is configured to navigate to a new dashboard state, the user will be redirected to that state when clicking on the widget.
- **Open URL:** If configured to open a URL, clicking on the widget will take the user to a specified web resource.
<br>
**Handle device activation toggle**
The example demonstrates how to dynamically call the 'turnOn' or 'turnOff' actions based on the 'active' status from the context. The actions are implemented using the following methods from the <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolapi" target="_blank">Scada Symbol API</a>:
@ -34,10 +61,9 @@ For more detailed guidelines on device interaction, consider reviewing the <a hr
```javascript
var active = ctx.values.active; // Current active status from context
var action = active ? 'turnOn' : 'turnOff'; // Determine action based on active status
var parameter = "any object or primitive"; // Parameter to pass with the action
// Call the action with observer callbacks for next and error handling
ctx.api.callAction(event, action, parameter, {
ctx.api.callAction(event, action, undefined, {
next: () => {
// Action succeeded; toggle the 'activate' status for debugging
ctx.api.setValue('activate', !active);
@ -47,4 +73,116 @@ ctx.api.callAction(event, action, parameter, {
ctx.api.setValue('activate', active);
}
});
{:copy-code}
```
<br>
This example utilizes two specific action behaviors, `turnOn` and `turnOff`, which interact with the target device based on the `active` status from the context:
1. **turnOn Behavior**
* **Type**: Action
* **Default Value**: `true`
* **Description**: This behavior is triggered when the device is activated. It sends a command to the target device to turn it on, indicating that the device should be in an operational state. The default value of `true` signifies that the action is intended to activate or enable the device.
2. **turnOf Behavior**
* **Type**: Action
* **Default Value**: `false`
* **Description**: This behavior is triggered when the device is deactivated. It sends a command to the target device to turn it off, indicating that the device should be in a non-operational state. The default value of `false` signifies that the action is intended to deactivate or disable the device.
##### Example with setting values for callAction function
**Temperature Adjustment Handler**
<br>
This JavaScript code demonstrates click actions for buttons that either increase or decrease the temperature value within a predefined range.
The behavior triggered by the click event updates the time series data for the temperature and ensures that the value stays within the defined limits.
The widget uses the <a href="${siteBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolcontext" target="_blank">ScadaSymbolContext API</a> to manage both temperature changes and the corresponding actions.
1. **Decrease Temperature (levelDown button)**:
* The user clicks the levelDown button to decrease the temperature. If the system is running, the temperature decreases by a step defined by `temperatureStep`, but it will never go below the `minTemperature`.
* The updateTemperatureState action is then triggered to update the time series value for temperature.
* In case of an error during the action, the temperature reverts to its previous value.
<br>
```javascript
if (ctx.values.running) {
// Retrieve the current temperature from the context values
var temperature = ctx.values.temperature;
// Retrieve the minimum allowable temperature from the context properties
var minTemperature = ctx.properties.minTemperature;
// Retrieve the step size by which the temperature should change
var step = ctx.properties.temperatureStep;
// Calculate the new temperature, ensuring it does not go below the minimum temperature
var newTemperature = Math.max(minTemperature, temperature - step);
// Set the new temperature value in the context
ctx.api.setValue('temperature', newTemperature);
// Trigger the action to update the temperature state, passing the new temperature value
ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {
// In case of an error, revert to the original temperature
error: () => {
ctx.api.setValue('temperature', temperature);
}
});
}
{:copy-code}
```
<br>
2. **Increase Temperature (levelUp button)**:
* The user clicks the levelUp button to increase the temperature. If the system is running, the temperature increases by the step, but will not exceed the `maxTemperature`.
* The action `updateTemperatureState` is called to update the time series value for temperature.
* If an error occurs, the previous temperature is restored to ensure consistency.
<br>
```javascript
if (ctx.values.running) {
// Retrieve the current temperature from the context values
var temperature = ctx.values.temperature;
// Retrieve the maximum and minimum allowable temperature from the context properties
var maxTemperature = ctx.properties.maxTemperature;
var minTemperature = ctx.properties.minTemperature;
// Retrieve the step size by which the temperature should change
var step = ctx.properties.temperatureStep;
// Calculate the new temperature:
// - Add the step to the current temperature
// - Ensure it doesn't exceed the maxTemperature
// - If temperature is null/undefined, use minTemperature as the initial value
var newTemperature = temperature || minTemperature === 0 ? Math.min(maxTemperature, temperature + step) : minTemperature;
// Set the new temperature value in the context
ctx.api.setValue('temperature', newTemperature);
// Trigger the action to update the temperature state, passing the new temperature value
ctx.api.callAction(event, 'updateTemperatureState', newTemperature, {
// In case of an error, revert to the original temperature
error: () => {
ctx.api.setValue('temperature', temperature);
}
});
}
{:copy-code}
```
<br>
The `updateTemperatureState` action is triggered to update the temperature value in the system. The action is configured with the following behavior:
*Action Behavior*: 'updateTemperatureState'
* **Type**: action
* **Value Type**: double
* **Default Settings**: Add time series
* **Time Series Key**: 'temperature'
This behavior updates the time series data for the temperature key, ensuring that the new temperature value is stored and displayed correctly.

59
ui-ngx/src/assets/help/en_US/scada/tag_state_render_fn.md

@ -21,43 +21,64 @@ A JavaScript function used to render SCADA symbol element with specific tag.
##### Examples
* Change the background of the element based on the value of the “active”
**Update element state based on device activity**
```javascript
if(ctx.values.active){
element.attr({fill: ctx.properties.activeColor});
} else {
element.attr({fill: ctx.properties.inactiveColor});
}
{:copy-code}
```
This JavaScript snippet demonstrates how to dynamically change the background color of an SVG element based on the `active` status from the context.
Additionally, it enables or disables the element’s click functionality depending on the `active` status.
If the device is active, the element is given the activeColor and click actions are allowed. Otherwise, it is assigned the inactiveColor and the click action is disabled.
* Enable and disable the “On” button based on the state of the "active" (avoid or prevent click action)
<br>
```javascript
if (ctx.values.active) {
ctx.api.disable(element);
} else {
// Context values
var active = ctx.values.active; // Device connectivity status
// Colors from context properties
var activeColor = ctx.properties.activeColor || '#4CAF50'; // Color for enabled state
var inactiveColor = ctx.properties.inactiveColor || '#A1ADB1'; // Color for disabled state
// Check if the device is active
if (active) {
// Set the background color to activeColor if active
element.attr({fill: activeColor});
// Enable the element to allow click actions
ctx.api.enable(element);
} else {
// Set the background color to inactiveColor if not active
element.attr({fill: inactiveColor});
// Disable the element to forbid click actions
ctx.api.disable(element);
}
{:copy-code}
```
* Smooth infinite rotation animation based on the value of the “active” with speed based on the value of the “speed”
<br>
**Smooth rotation based on activity and speed**
This JavaScript snippet creates a smooth, infinite rotation animation for an element based on the `active` status and adjusts the animation speed dynamically according to the `speed` value.
If the element is active, the animation starts or continues rotating with a speed proportional to the speed value. If inactive, the animation pauses.
<br>
```javascript
// Get the 'active' status and the current speed
var on = ctx.values.active;
var speed = ctx.values.speed ? ctx.values.speed / 60 : 1;
var animation = ctx.api.cssAnimation(element);
var animation = ctx.api.cssAnimation(element); // Retrieve any existing animation on the element
var speed = ctx.values.speed ? ctx.values.speed / 60 : 1; // Calculate speed, default to 1 if not provided
var animationDuration = 2000; // Duration for one full rotation (optional, can be adjusted)
var rotationAngle = 360; // Full rotation in degrees
if (on) {
// If active, either create a new rotation animation or adjust the existing one
if (!animation) {
animation = ctx.api.cssAnimate(element, 2000)
.rotate(360).loop().speed(speed);
animation = ctx.api.cssAnimate(element) // Create new animation if not already active
.rotate(rotationAngle) // Set rotation angle to 360 degrees
.loop() // Loop the animation infinitely
.speed(speed); // Adjust speed based on the 'speed' value from the context
} else {
animation.speed(speed).play();
animation.speed(speed).play(); // If animation exists, adjust its speed and continue playing
}
} else {
// If inactive, pause the animation
if (animation) {
animation.pause();
}

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

@ -1211,7 +1211,7 @@
"layout-type-default": "Default",
"layout-type-scada": "SCADA",
"layout-type-divider": "Divider",
"layout-settings-type": "Layout settings: {{ type }}",
"layout-settings-type": "Layout settings: {{ type }} breakpoint",
"columns-count": "Columns count",
"columns-count-required": "Columns count is required.",
"min-columns-count-message": "Only 10 minimum column count is allowed.",
@ -3036,6 +3036,7 @@
"grpc-max-pings-without-data-required": "Max pings without data is required",
"grpc-max-pings-without-data-min": "Max pings without data can not be less then 1",
"grpc-max-pings-without-data-pattern": "Max pings without data is not valid",
"info": "Info",
"identity": "Identity",
"inactivity-check-period-seconds": "Inactivity check period (in sec)",
"inactivity-check-period-seconds-required": "Inactivity check period is required",
@ -3317,7 +3318,7 @@
"check-connectors-configuration-min": "Check connectors configuration can not be less then 1",
"check-connectors-configuration-pattern": "Check connectors configuration is not valid",
"add": "Add command",
"timeout": "Timeout",
"timeout": "Timeout (in sec)",
"timeout-ms": "Timeout (in ms)",
"timeout-required": "Timeout is required",
"timeout-min": "Timeout can not be less then 1",

7
ui-ngx/src/assets/metadata/connector-default-configs/modbus.json

@ -3,7 +3,6 @@
"master": {
"slaves": [
{
"name": "Slave 1",
"host": "127.0.0.1",
"port": 5021,
"type": "tcp",
@ -231,10 +230,10 @@
"attributes": [
{
"address": 5,
"type": "string",
"tag": "sm",
"type": "8int",
"tag": "coil",
"objectsCount": 1,
"value": "12"
"value": 0
}
],
"timeseries": [],

Loading…
Cancel
Save