Browse Source

Merge branch 'master' into task/4114-gateway-version-compatibility

pull/11516/head
Max Petrov 2 years ago
committed by GitHub
parent
commit
410c974841
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      application/src/main/resources/thingsboard.yml
  2. 32
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplate.java
  3. 2
      common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqSettings.java
  4. 128
      common/queue/src/test/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplateTest.java
  5. 1
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  6. 26
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java
  7. 50
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  8. 9
      ui-ngx/src/app/core/api/alias-controller.ts
  9. 1
      ui-ngx/src/app/core/api/widget-api.models.ts
  10. 12
      ui-ngx/src/app/core/services/dashboard-utils.service.ts
  11. 6
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  12. 85
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html
  13. 426
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/basic/gateway-basic-configuration.component.ts
  14. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/configuration/gateway-configuration.component.ts
  15. 3
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html
  16. 16
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts
  17. 3
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html
  18. 7
      ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts
  19. 253
      ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts
  20. 35
      ui-ngx/src/app/shared/components/js-func.component.ts
  21. 13
      ui-ngx/src/app/shared/models/ace/ace.models.ts
  22. 1
      ui-ngx/src/app/shared/models/dashboard.models.ts

1
application/src/main/resources/thingsboard.yml

@ -1658,6 +1658,7 @@ queue:
connection_timeout: "${TB_QUEUE_RABBIT_MQ_CONNECTION_TIMEOUT:60000}"
# RabbitMQ has a timeout for connection handshake. When clients run in heavily constrained environments, it may be necessary to increase the timeout
handshake_timeout: "${TB_QUEUE_RABBIT_MQ_HANDSHAKE_TIMEOUT:10000}"
max_poll_messages: "${TB_QUEUE_RABBIT_MQ_MAX_POLL_MESSAGES:1}"
queue-properties:
# RabbitMQ properties for Rule Engine queues
rule-engine: "${TB_QUEUE_RABBIT_MQ_RE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}"

32
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplate.java

@ -20,6 +20,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.GetResponse;
import java.util.ArrayList;
import java.util.Collection;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
@ -31,7 +33,6 @@ import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
@ -44,6 +45,7 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
private final TbQueueMsgDecoder<T> decoder;
private final Channel channel;
private final Connection connection;
private final int maxPollMessages;
private volatile Set<String> queues;
@ -51,6 +53,7 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
super(topic);
this.admin = admin;
this.decoder = decoder;
this.maxPollMessages = rabbitMqSettings.getMaxPollMessages();
try {
connection = rabbitMqSettings.getConnectionFactory().newConnection();
} catch (IOException | TimeoutException e) {
@ -70,13 +73,19 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
protected List<GetResponse> doPoll(long durationInMillis) {
List<GetResponse> result = queues.stream()
.map(queue -> {
try {
return channel.basicGet(queue, false);
} catch (IOException e) {
log.error("Failed to get messages from queue: [{}]", queue);
throw new RuntimeException("Failed to get messages from queue.", e);
List<GetResponse> messages = new ArrayList<>();
for (int i = 0; i < maxPollMessages; i++) {
GetResponse response = doQueuePoll(queue);
if (response == null) {
break;
}
messages.add(response);
}
}).filter(Objects::nonNull).collect(Collectors.toList());
return messages;
})
.filter(r -> !r.isEmpty())
.flatMap(Collection::stream)
.collect(Collectors.toList());
if (result.size() > 0) {
return result;
} else {
@ -84,6 +93,15 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
}
}
protected GetResponse doQueuePoll(String queue) {
try {
return channel.basicGet(queue, false);
} catch (IOException e) {
log.error("Failed to get messages from queue: [{}]", queue);
throw new RuntimeException("Failed to get messages from queue.", e);
}
}
@Override
protected void doSubscribe(List<String> topicNames) {
queues = partitions.stream()

2
common/queue/src/main/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqSettings.java

@ -46,6 +46,8 @@ public class TbRabbitMqSettings {
private int connectionTimeout;
@Value("${queue.rabbitmq.handshake_timeout:}")
private int handshakeTimeout;
@Value("${queue.rabbitmq.max_poll_messages:1}")
private int maxPollMessages;
private ConnectionFactory connectionFactory;

128
common/queue/src/test/java/org/thingsboard/server/queue/rabbitmq/TbRabbitMqConsumerTemplateTest.java

@ -0,0 +1,128 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.queue.rabbitmq;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.GetResponse;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueMsgDecoder;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
@ExtendWith(MockitoExtension.class)
class TbRabbitMqConsumerTemplateTest {
private static final String TOPIC = "some-topic";
@Mock
private TbQueueAdmin admin;
@Mock
private ConnectionFactory connectionFactory;
@Mock
private TbQueueMsgDecoder<DefaultTbQueueMsg> decoder;
@Mock
private Connection connection;
@Mock
private Channel channel;
@Mock
private TopicPartitionInfo partition;
@Mock
private GetResponse getResponse;
private TbRabbitMqConsumerTemplate<DefaultTbQueueMsg> consumer;
private void setUpConsumerWithMaxPollMessages(int maxPollMessages) throws Exception {
when(connectionFactory.newConnection()).thenReturn(connection);
when(connection.createChannel()).thenReturn(channel);
TbRabbitMqSettings settings = new TbRabbitMqSettings();
settings.setMaxPollMessages(maxPollMessages);
settings.setConnectionFactory(connectionFactory);
consumer = new TbRabbitMqConsumerTemplate<>(admin, settings, TOPIC, decoder);
when(partition.getFullTopicName()).thenReturn(TOPIC);
consumer.subscribe(Set.of(partition));
}
@Test
void pollWithMax5PollMessagesReturnsEmptyListIfNoMessages() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(null);
assertThat(consumer.poll(0L)).isEmpty();
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax5PollMessagesReturns5MessagesIfQueueContains5() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(5);
verify(channel, times(5)).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax1PollMessageReturns1MessageIfQueueContainsMore() throws Exception {
setUpConsumerWithMaxPollMessages(1);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(1);
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax3PollMessagesReturns2MessagesIfQueueContains2() throws Exception {
setUpConsumerWithMaxPollMessages(3);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse, getResponse, null);
assertThat(consumer.poll(0L)).hasSize(2);
verify(channel, times(3)).basicGet(anyString(), anyBoolean());
}
private byte[] newMessageBody() {
return ("{\"key\": \"" + UUID.randomUUID() + "\"}").getBytes(StandardCharsets.UTF_8);
}
}

1
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -1063,6 +1063,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
log.debug("[{}][{}][{}] IOException: {}", sessionId,
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceId).orElse(null),
Optional.ofNullable(this.deviceSessionCtx.getDeviceInfo()).map(TransportDeviceInfo::getDeviceName).orElse(""),
cause.getMessage(),
cause);
} else if (log.isInfoEnabled()) {
log.info("[{}][{}][{}] IOException: {}", sessionId,

26
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttHandler.java

@ -15,10 +15,30 @@
*/
package org.thingsboard.mqtt;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.concurrent.Future;
import io.netty.buffer.ByteBuf;
public interface MqttHandler {
ListenableFuture<Void> onMessage(String topic, ByteBuf payload);
/**
* Changing ListenableFuture to Future allows you to choose CompletableFuture,
* which gives developers the freedom to choose the orchestration method.
* CompletableFuture is a newer, more evolved version that eliminates callback hell,
* is easier to use, and comes with the JDK. jdk 1.8 was previously used with the Before JDK1.8,
* use ListenableFuture, after that, it is recommended to use CompletableFuture.
* ListenableFuture It's still written that way.{@link MqttMessageListener#onMessage(topic, payload)}
* public ListenableFuture<Void> onMessage(String topic, ByteBuf message) {
* log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic);
* events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8)));
* return Futures.immediateVoidFuture();
* }
* CompletableFuture It's like this.
* public CompletableFuture<Void> onMessage(String topic, ByteBuf message) {
* log.info("MQTT message [{}], topic [{}]", message.toString(StandardCharsets.UTF_8), topic);
* events.add(new MqttEvent(topic, message.toString(StandardCharsets.UTF_8)));
* return CompletableFuture.completedFuture(null);
* }
* This change does not affect the system's current use of ListenableFuture so that it is free to choose between ListenableFuture or
* CompletableFuture in new development.
*/
Future<Void> onMessage(String topic, ByteBuf payload);
}

50
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

9
ui-ngx/src/app/core/api/alias-controller.ts

@ -68,10 +68,11 @@ export class AliasController implements IAliasController {
private translate: TranslateService,
private stateControllerHolder: StateControllerHolder,
private origEntityAliases: EntityAliases,
private origFilters: Filters) {
private origFilters: Filters,
private origUserFilters?: Filters) {
this.entityAliases = deepClone(this.origEntityAliases) || {};
this.filters = deepClone(this.origFilters) || {};
this.userFilters = {};
this.userFilters = deepClone(this.origUserFilters) || {};
}
updateEntityAliases(newEntityAliases: EntityAliases) {
@ -166,6 +167,10 @@ export class AliasController implements IAliasController {
return this.filters;
}
getUserFilters(): Filters {
return this.userFilters;
}
getFilterInfo(filterId: string): FilterInfo {
if (this.userFilters[filterId]) {
return this.userFilters[filterId];

1
ui-ngx/src/app/core/api/widget-api.models.ts

@ -137,6 +137,7 @@ export interface IAliasController {
resolveAlarmSource(alarmSource: Datasource): Observable<Datasource>;
getEntityAliases(): EntityAliases;
getFilters(): Filters;
getUserFilters(): Filters;
getFilterInfo(filterId: string): FilterInfo;
getKeyFilters(filterId: string): Array<KeyFilter>;
updateCurrentAliasEntity(aliasId: string, currentEntity: EntityInfo): void;

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

@ -31,7 +31,7 @@ import {
DashboardLayoutsInfo,
DashboardState,
DashboardStateLayouts,
GridSettings,
GridSettings, LayoutType,
WidgetLayout
} from '@shared/models/dashboard.models';
import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils';
@ -406,6 +406,7 @@ export class DashboardUtilsService {
private createDefaultGridSettings(): GridSettings {
return {
layoutType: LayoutType.default,
backgroundColor: '#eeeeee',
columns: 24,
margin: 10,
@ -491,12 +492,15 @@ export class DashboardUtilsService {
if (!layout.gridSettings) {
layout.gridSettings = this.createDefaultGridSettings();
}
if (layout.gridSettings.margins && layout.gridSettings.margins.length === 2) {
layout.gridSettings.margin = layout.gridSettings.margins[0];
delete layout.gridSettings.margins;
if ((layout.gridSettings as any).margins && (layout.gridSettings as any).margins.length === 2) {
layout.gridSettings.margin = (layout.gridSettings as any).margins[0];
delete (layout.gridSettings as any).margins;
}
layout.gridSettings.outerMargin = isDefined(layout.gridSettings.outerMargin) ? layout.gridSettings.outerMargin : true;
layout.gridSettings.margin = isDefined(layout.gridSettings.margin) ? layout.gridSettings.margin : 10;
if (!layout.gridSettings.layoutType) {
layout.gridSettings.layoutType = LayoutType.default;
}
}
public setLayouts(dashboard: Dashboard, targetState: string, newLayouts: DashboardStateLayouts) {

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

@ -522,7 +522,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.translate,
() => this.dashboardCtx.stateController,
this.dashboardConfiguration.entityAliases,
this.dashboardConfiguration.filters);
this.dashboardConfiguration.filters,
this.parentDashboard?.aliasController.getUserFilters());
this.updateDashboardCss();
@ -1168,8 +1169,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.filtersUpdated();
this.updateLayouts();
}
} else if (!this.widgetEditMode) {
this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
}
}
}
@ -1216,6 +1215,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.setEditMode(false, false);
} else {
let reInitDashboard = false;
this.dashboard.configuration.timewindow = this.dashboardCtx.dashboardTimewindow;
this.dashboardService.saveDashboard(this.dashboard).pipe(
catchError((err) => {
if (err.status === HttpStatusCode.Conflict) {

85
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-settings-dialog.component.html

@ -119,20 +119,15 @@
</div>
<div class="tb-form-row space-between">
<div translate>dashboard.columns-count</div>
<mat-form-field *ngIf="!isScada()"
appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field *ngIf="!isScada()" appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('columns').invalid && gridSettingsFormGroup.get('columns').touched)"
[matTooltip]="(gridSettingsFormGroup.get('columns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('columns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate">
<input matInput formControlName="columns" type="number" min="10" max="1008" step="1"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('columns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('columns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"
*ngIf="gridSettingsFormGroup.get('columns').invalid && gridSettingsFormGroup.get('columns').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<mat-form-field *ngIf="isScada()"
class="medium-width" appearance="outline" subscriptSizing="dynamic">
@ -145,37 +140,29 @@
</div>
<div class="tb-form-row space-between">
<div translate>dashboard.min-layout-width</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('minColumns').invalid && gridSettingsFormGroup.get('minColumns').touched)"
[matTooltip]="(gridSettingsFormGroup.get('minColumns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('minColumns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate">
<input matInput formControlName="minColumns" type="number" min="10" max="1008" step="1"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('minColumns').hasError('required') ? 'dashboard.columns-count-required' :
(gridSettingsFormGroup.get('minColumns').hasError('min') ? 'dashboard.min-columns-count-message' : 'dashboard.max-columns-count-message')) | translate"
*ngIf="gridSettingsFormGroup.get('minColumns').invalid && gridSettingsFormGroup.get('minColumns').touched"
class="tb-error">
warning
</mat-icon>
<span translate matSuffix>dashboard.columns-suffix</span>
</mat-form-field>
</div>
<div *ngIf="!isScada()" class="tb-form-row space-between">
<div translate>dashboard.widgets-margins</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched)"
[matTooltip]="(gridSettingsFormGroup.get('margin').hasError('required') ? 'dashboard.margin-required' :
(gridSettingsFormGroup.get('margin').hasError('min') ? 'dashboard.min-margin-message' : 'dashboard.max-margin-message')) | translate">
<input matInput formControlName="margin" type="number" min="0" max="50" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('margin').hasError('required') ? 'dashboard.margin-required' :
(gridSettingsFormGroup.get('margin').hasError('min') ? 'dashboard.min-margin-message' : 'dashboard.max-margin-message')) | translate"
*ngIf="gridSettingsFormGroup.get('margin').invalid && gridSettingsFormGroup.get('margin').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div *ngIf="!isScada()" class="tb-form-row">
@ -190,20 +177,16 @@
</div>
<div *ngIf="isDefaultLayout && !isDefaultBreakpoint" class="tb-form-row space-between">
<div translate>dashboard.row-height</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched)"
[matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate">
<input matInput formControlName="rowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('rowHeight').hasError('required') ? 'dashboard.row-height-required' :
(gridSettingsFormGroup.get('rowHeight').hasError('min') ?
'dashboard.min-row-height-message' : 'dashboard.max-row-height-message')) | translate"
*ngIf="gridSettingsFormGroup.get('rowHeight').invalid && gridSettingsFormGroup.get('rowHeight').touched"
class="tb-error">
warning
</mat-icon>
<span matSuffix>px</span>
</mat-form-field>
</div>
@ -245,20 +228,16 @@
</div>
<div *ngIf="!isScada()" class="tb-form-row space-between">
<div translate>dashboard.mobile-row-height</div>
<mat-form-field appearance="outline" class="number medium-width tb-suffix-absolute" subscriptSizing="dynamic">
<mat-form-field appearance="outline" class="number medium-width" subscriptSizing="dynamic"
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltipDisabled]="!(gridSettingsFormGroup.get('mobileRowHeight').invalid && gridSettingsFormGroup.get('mobileRowHeight').touched)"
[matTooltip]="(gridSettingsFormGroup.get('mobileRowHeight').hasError('required') ? 'dashboard.mobile-row-height-required' :
(gridSettingsFormGroup.get('mobileRowHeight').hasError('min') ?
'dashboard.min-mobile-row-height-message' : 'dashboard.max-mobile-row-height-message')) | translate">
<input matInput formControlName="mobileRowHeight" type="number" min="5" max="200" step="any"
required
placeholder="{{ 'widget-config.set' | translate }}">
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="(gridSettingsFormGroup.get('mobileRowHeight').hasError('required') ? 'dashboard.mobile-row-height-required' :
(gridSettingsFormGroup.get('mobileRowHeight').hasError('min') ?
'dashboard.min-mobile-row-height-message' : 'dashboard.max-mobile-row-height-message')) | translate"
*ngIf="gridSettingsFormGroup.get('mobileRowHeight').invalid && gridSettingsFormGroup.get('mobileRowHeight').touched"
class="tb-error">
warning
</mat-icon>
<span matSuffix>px</span>
</mat-form-field>
</div>

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

@ -43,7 +43,7 @@ import {
} from '@home/components/widget/lib/gateway/gateway-remote-configuration-dialog';
import { DeviceService } from '@core/http/device.service';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { take, takeUntil } from 'rxjs/operators';
import { DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import {
GatewayLogLevel,
@ -61,7 +61,9 @@ import { CommonModule } from '@angular/common';
import { coerceBoolean } from '@shared/decorators/coercion';
import {
GatewayConfigCommand,
GatewayConfigValue, LogConfig
GatewayConfigSecurity,
GatewayConfigValue,
LogConfig
} from '@home/components/widget/lib/gateway/configuration/models/gateway-configuration.models';
@Component({
@ -99,13 +101,14 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
initialCredentialsUpdated = new EventEmitter<DeviceCredentials>();
StorageTypes = StorageTypes;
storageTypes = Object.values(StorageTypes) as StorageTypes[];
storageTypes = Object.values(StorageTypes);
storageTypesTranslationMap = StorageTypesTranslationMap;
logSavingPeriods = LogSavingPeriodTranslations;
localLogsConfigs = Object.keys(LocalLogsConfigs) as LocalLogsConfigs[];
localLogsConfigTranslateMap = LocalLogsConfigTranslateMap;
securityTypes = GecurityTypesTranslationsMap;
gatewayLogLevel = Object.values(GatewayLogLevel);
logSelector: FormControl;
basicFormGroup: FormGroup;
@ -119,6 +122,7 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
private cd: ChangeDetectorRef,
private dialog: MatDialog) {
this.initBasicFormGroup();
this.observeFormChanges();
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
@ -142,14 +146,12 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
writeValue(basicConfig: GatewayConfigValue): void {
this.basicFormGroup.patchValue(basicConfig, {emitEvent: false});
if (basicConfig?.thingsboard?.security) {
this.checkAndFetchCredentials(basicConfig.thingsboard.security);
}
this.checkAndFetchCredentials(basicConfig?.thingsboard?.security ?? {} as GatewayConfigSecurity);
if (basicConfig?.grpc) {
this.toggleRpcFields(basicConfig.grpc.enabled);
}
const commands = basicConfig?.thingsboard?.statistics?.commands || [];
commands.forEach(command => this.addCommand(command, false));
const commands = basicConfig?.thingsboard?.statistics?.commands ?? [];
commands.forEach((command: GatewayConfigCommand) => this.addCommand(command, false));
}
validate(): ValidationErrors | null {
@ -251,7 +253,7 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
data: {
gatewayName: gateway.name
}
}).afterClosed().subscribe(
}).afterClosed().pipe(take(1)).subscribe(
(res) => {
if (!res) {
this.basicFormGroup.get('thingsboard.remoteConfiguration').setValue(true, {emitEvent: false});
@ -273,214 +275,294 @@ export class GatewayBasicConfigurationComponent implements OnDestroy, ControlVal
this.commandFormArray().push(commandFormGroup, { emitEvent });
}
private initBasicFormGroup(): void {
this.basicFormGroup = this.fb.group({
thingsboard: this.fb.group({
host: [window.location.hostname, [Validators.required, Validators.pattern(/^[^\s]+$/)]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false, []],
remoteConfiguration: [true, []],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
statistics: this.fb.group({
enable: [true, []],
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(60), Validators.pattern(/^-?[0-9]+$/)]],
commands: this.fb.array([], [])
}),
maxPayloadSizeBytes: [8196, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [50, [Validators.required, Validators.min(10), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [true, []],
checkingDeviceActivity: this.fb.group({
checkDeviceInactivity: [false, []],
inactivityTimeoutSeconds: [200, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
}),
security: this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
clientId: [null, [Validators.pattern(/^[^.\s]+$/)]],
username: [null, [Validators.pattern(/^[^.\s]+$/)]],
password: [null, [Validators.pattern(/^[^.\s]+$/)]],
caCert: [null, []],
cert: [null, []],
privateKey: [null, []],
}),
qos: [1, [Validators.min(0), Validators.max(1), Validators.required, Validators.pattern(/^[^.\s]+$/)]]
}),
storage: this.fb.group({
type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [
100, [Validators.min(1),
Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]
],
max_records_count: [
100000,
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required, Validators.pattern(/^[^.\s]+$/)]
],
data_folder_path: ['./data/', [Validators.required]],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_file_path: ['./data/data.db', [Validators.required]],
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
thingsboard: this.initThingsboardFormGroup(),
storage: this.initStorageFormGroup(),
grpc: this.initGrpcFormGroup(),
connectors: this.fb.array([]),
logs: this.initLogsFormGroup(),
});
}
private initThingsboardFormGroup(): FormGroup {
return this.fb.group({
host: [window.location.hostname, [Validators.required, Validators.pattern(/^[^\s]+$/)]],
port: [1883, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
remoteShell: [false],
remoteConfiguration: [true],
checkConnectorsConfigurationInSeconds: [60, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
statistics: this.fb.group({
enable: [true],
statsSendPeriodInSeconds: [3600, [Validators.required, Validators.min(60), Validators.pattern(/^-?[0-9]+$/)]],
commands: this.fb.array([])
}),
grpc: this.fb.group({
enabled: [false, []],
serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepalivePermitWithoutCalls: [true, []],
maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
maxPayloadSizeBytes: [8196, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
minPackSendDelayMS: [50, [Validators.required, Validators.min(10), Validators.pattern(/^-?[0-9]+$/)]],
minPackSizeToSend: [500, [Validators.required, Validators.min(100), Validators.pattern(/^-?[0-9]+$/)]],
handleDeviceRenaming: [true],
checkingDeviceActivity: this.initCheckingDeviceActivityFormGroup(),
security: this.initSecurityFormGroup(),
qos: [1, [Validators.required, Validators.min(0), Validators.max(1), Validators.pattern(/^[^.\s]+$/)]]
});
}
private initStorageFormGroup(): FormGroup {
return this.fb.group({
type: [StorageTypes.MEMORY, [Validators.required]],
read_records_count: [100, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_count: [100000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_folder_path: ['./data/', [Validators.required]],
max_file_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_read_records_count: [10, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
max_records_per_file: [10000, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
data_file_path: ['./data/data.db', [Validators.required]],
messages_ttl_check_in_hours: [1, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
messages_ttl_in_days: [7, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
});
}
private initGrpcFormGroup(): FormGroup {
return this.fb.group({
enabled: [false],
serverPort: [9595, [Validators.required, Validators.min(1), Validators.max(65535), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepAliveTimeoutMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
keepalivePermitWithoutCalls: [true],
maxPingsWithoutData: [0, [Validators.required, Validators.min(0), Validators.pattern(/^-?[0-9]+$/)]],
minTimeBetweenPingsMs: [10000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
minPingIntervalWithoutDataMs: [5000, [Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
});
}
private initLogsFormGroup(): FormGroup {
return this.fb.group({
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
logFormat: [
'%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
[Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]
],
type: ['remote', [Validators.required]],
remote: this.fb.group({
enabled: [false],
logLevel: [GatewayLogLevel.INFO, [Validators.required]]
}),
connectors: this.fb.array([]),
logs: this.fb.group({
dateFormat: ['%Y-%m-%d %H:%M:%S', [Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
logFormat: ['%(asctime)s - |%(levelname)s| - [%(filename)s] - %(module)s - %(funcName)s - %(lineno)d - %(message)s',
[Validators.required, Validators.pattern(/^[^\s].*[^\s]$/)]],
type: ['remote', [Validators.required]],
remote: this.fb.group({
enabled: [false],
logLevel: [GatewayLogLevel.INFO, [Validators.required]],
}),
local: this.fb.group({})
})
local: this.fb.group({})
});
}
private initCheckingDeviceActivityFormGroup(): FormGroup {
return this.fb.group({
checkDeviceInactivity: [false],
inactivityTimeoutSeconds: [200, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]],
inactivityCheckPeriodSeconds: [500, [Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]]
});
}
this.basicFormGroup.get('thingsboard.security.password').valueChanges.subscribe(password => {
private initSecurityFormGroup(): FormGroup {
return this.fb.group({
type: [SecurityTypes.ACCESS_TOKEN, [Validators.required]],
accessToken: [null, [Validators.required, Validators.pattern(/^[^.\s]+$/)]],
clientId: [null, [Validators.pattern(/^[^.\s]+$/)]],
username: [null, [Validators.pattern(/^[^.\s]+$/)]],
password: [null, [Validators.pattern(/^[^.\s]+$/)]],
caCert: [null],
cert: [null],
privateKey: [null]
});
}
private observeFormChanges(): void {
this.observeSecurityPasswordChanges();
this.observeRemoteConfigurationChanges();
this.observeDeviceActivityChanges();
this.observeSecurityTypeChanges();
this.observeStorageTypeChanges();
}
private observeSecurityPasswordChanges(): void {
const securityUsername = this.basicFormGroup.get('thingsboard.security.username');
this.basicFormGroup.get('thingsboard.security.password').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(password => {
if (password && password !== '') {
this.basicFormGroup.get('thingsboard.security.username').setValidators([Validators.required]);
securityUsername.setValidators([Validators.required]);
} else {
this.basicFormGroup.get('thingsboard.security.username').clearValidators();
securityUsername.clearValidators();
}
this.basicFormGroup.get('thingsboard.security.username').updateValueAndValidity({emitEvent: false});
securityUsername.updateValueAndValidity({ emitEvent: false });
});
}
this.toggleRpcFields(false);
this.basicFormGroup.get('thingsboard.remoteConfiguration').valueChanges.subscribe(enabled => {
private observeRemoteConfigurationChanges(): void {
this.basicFormGroup.get('thingsboard.remoteConfiguration').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(enabled => {
if (!enabled) {
this.openConfigurationConfirmDialog();
}
});
this.logSelector = this.fb.control(LocalLogsConfigs.service);
for (const localLogsConfigsKey of Object.keys(LocalLogsConfigs)) {
this.addLocalLogConfig(localLogsConfigsKey, {} as LogConfig);
for (const key of Object.keys(LocalLogsConfigs)) {
this.addLocalLogConfig(key, {} as LogConfig);
}
}
private observeDeviceActivityChanges(): void {
const checkingDeviceActivityGroup = this.basicFormGroup.get('thingsboard.checkingDeviceActivity') as FormGroup;
checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.subscribe(enabled => {
checkingDeviceActivityGroup.get('checkDeviceInactivity').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(enabled => {
checkingDeviceActivityGroup.updateValueAndValidity();
const validators = [Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)];
if (enabled) {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds')
.setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds')
.setValidators([Validators.min(1), Validators.required, Validators.pattern(/^-?[0-9]+$/)]);
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').setValidators(validators);
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').setValidators(validators);
} else {
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').clearValidators();
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').clearValidators();
}
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').updateValueAndValidity({emitEvent: false});
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({emitEvent: false});
checkingDeviceActivityGroup.get('inactivityTimeoutSeconds').updateValueAndValidity({ emitEvent: false });
checkingDeviceActivityGroup.get('inactivityCheckPeriodSeconds').updateValueAndValidity({ emitEvent: false });
});
this.basicFormGroup.get('grpc.enabled').valueChanges.subscribe(value => {
this.basicFormGroup.get('grpc.enabled').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(value => {
this.toggleRpcFields(value);
});
}
private observeSecurityTypeChanges(): void {
const securityGroup = this.basicFormGroup.get('thingsboard.security') as FormGroup;
securityGroup.get('type').valueChanges.subscribe(type => {
securityGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.removeAllSecurityValidators();
if (type === SecurityTypes.ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
securityGroup.get('accessToken').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_PRIVATE_KEY) {
securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity();
securityGroup.get('privateKey').addValidators([Validators.required]);
securityGroup.get('privateKey').updateValueAndValidity();
securityGroup.get('cert').addValidators([Validators.required]);
securityGroup.get('cert').updateValueAndValidity();
} else if (type === SecurityTypes.TLS_ACCESS_TOKEN) {
securityGroup.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
securityGroup.get('accessToken').updateValueAndValidity();
securityGroup.get('caCert').addValidators([Validators.required]);
securityGroup.get('caCert').updateValueAndValidity();
} else if (type === SecurityTypes.USERNAME_PASSWORD) {
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
switch (type) {
case SecurityTypes.ACCESS_TOKEN:
this.addAccessTokenValidators(securityGroup);
break;
case SecurityTypes.TLS_PRIVATE_KEY:
this.addTlsPrivateKeyValidators(securityGroup);
break;
case SecurityTypes.TLS_ACCESS_TOKEN:
this.addTlsAccessTokenValidators(securityGroup);
break;
case SecurityTypes.USERNAME_PASSWORD:
securityGroup.addValidators([this.atLeastOneRequired(Validators.required, ['clientId', 'username'])]);
break;
}
securityGroup.updateValueAndValidity();
});
securityGroup.get('caCert').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('privateKey').valueChanges.subscribe(() => this.cd.detectChanges());
securityGroup.get('cert').valueChanges.subscribe(() => this.cd.detectChanges());
['caCert', 'privateKey', 'cert'].forEach(field => {
securityGroup.get(field).valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => this.cd.detectChanges());
});
}
private observeStorageTypeChanges(): void {
const storageGroup = this.basicFormGroup.get('storage') as FormGroup;
storageGroup.get('type').valueChanges.subscribe(type => {
storageGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(type => {
this.removeAllStorageValidators();
if (type === StorageTypes.MEMORY) {
storageGroup.get('read_records_count').addValidators(
[Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
storageGroup.get('max_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_count').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.FILE) {
storageGroup.get('data_folder_path').addValidators([Validators.required]);
storageGroup.get('max_file_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_read_records_count').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('max_records_per_file').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('data_folder_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_file_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_read_records_count').updateValueAndValidity({emitEvent: false});
storageGroup.get('max_records_per_file').updateValueAndValidity({emitEvent: false});
} else if (type === StorageTypes.SQLITE) {
storageGroup.get('data_file_path').addValidators([Validators.required]);
storageGroup.get('messages_ttl_check_in_hours').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('messages_ttl_in_days').addValidators(
[Validators.min(1), Validators.pattern(/^-?[0-9]+$/), Validators.required]);
storageGroup.get('data_file_path').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_check_in_hours').updateValueAndValidity({emitEvent: false});
storageGroup.get('messages_ttl_in_days').updateValueAndValidity({emitEvent: false});
switch (type) {
case StorageTypes.MEMORY:
this.addMemoryStorageValidators(storageGroup);
break;
case StorageTypes.FILE:
this.addFileStorageValidators(storageGroup);
break;
case StorageTypes.SQLITE:
this.addSqliteStorageValidators(storageGroup);
break;
}
});
}
private checkAndFetchCredentials(security: any = {}): void {
if (security.type !== SecurityTypes.TLS_PRIVATE_KEY) {
this.deviceService.getDeviceCredentials(this.device.id).subscribe(credentials => {
this.initialCredentialsUpdated.emit(credentials);
if (credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN || security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.basicFormGroup.get('thingsboard.security.type')
.setValue(security.type === SecurityTypes.TLS_ACCESS_TOKEN
? SecurityTypes.TLS_ACCESS_TOKEN
: SecurityTypes.ACCESS_TOKEN, {emitEvent: false});
this.basicFormGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId, {emitEvent: false});
if(security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.basicFormGroup.get('thingsboard.security.caCert').setValue(security.caCert, {emitEvent: false});
}
} else if (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
const parsedValue = JSON.parse(credentials.credentialsValue);
this.basicFormGroup.get('thingsboard.security.type').setValue(SecurityTypes.USERNAME_PASSWORD, {emitEvent: false});
this.basicFormGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId, {emitEvent: false});
this.basicFormGroup.get('thingsboard.security.username').setValue(parsedValue.userName, {emitEvent: false});
this.basicFormGroup.get('thingsboard.security.password')
.setValue(parsedValue.password, {emitEvent: false});
} else if (credentials.credentialsType === DeviceCredentialsType.X509_CERTIFICATE) {
//if sertificate is present set sertificate as present
}
});
private addAccessTokenValidators(group: FormGroup): void {
group.get('accessToken').addValidators([Validators.required, Validators.pattern(/^[^.\s]+$/)]);
group.get('accessToken').updateValueAndValidity();
}
private addTlsPrivateKeyValidators(group: FormGroup): void {
['caCert', 'privateKey', 'cert'].forEach(field => {
group.get(field).addValidators([Validators.required]);
group.get(field).updateValueAndValidity();
});
}
private addTlsAccessTokenValidators(group: FormGroup): void {
this.addAccessTokenValidators(group);
group.get('caCert').addValidators([Validators.required]);
group.get('caCert').updateValueAndValidity();
}
private addMemoryStorageValidators(group: FormGroup): void {
group.get('read_records_count').addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get('max_records_count').addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get('read_records_count').updateValueAndValidity({ emitEvent: false });
group.get('max_records_count').updateValueAndValidity({ emitEvent: false });
}
private addFileStorageValidators(group: FormGroup): void {
['data_folder_path', '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 => {
group.get(field).addValidators([Validators.required, Validators.min(1), Validators.pattern(/^-?[0-9]+$/)]);
group.get(field).updateValueAndValidity({ emitEvent: false });
});
}
private checkAndFetchCredentials(security: GatewayConfigSecurity): void {
if (security.type === SecurityTypes.TLS_PRIVATE_KEY) {
return;
}
this.deviceService.getDeviceCredentials(this.device.id).pipe(takeUntil(this.destroy$)).subscribe(credentials => {
this.initialCredentialsUpdated.emit(credentials);
this.updateSecurityType(security, credentials);
this.updateCredentials(credentials, security);
});
}
private updateSecurityType(security, credentials: DeviceCredentials): void {
const isAccessToken = credentials.credentialsType === DeviceCredentialsType.ACCESS_TOKEN
|| security.type === SecurityTypes.TLS_ACCESS_TOKEN;
const securityType = isAccessToken
? (security.type === SecurityTypes.TLS_ACCESS_TOKEN ? SecurityTypes.TLS_ACCESS_TOKEN : SecurityTypes.ACCESS_TOKEN)
: (credentials.credentialsType === DeviceCredentialsType.MQTT_BASIC ? SecurityTypes.USERNAME_PASSWORD : null);
if (securityType) {
this.basicFormGroup.get('thingsboard.security.type').setValue(securityType, { emitEvent: false });
}
}
private updateCredentials(credentials: DeviceCredentials, security: GatewayConfigSecurity): void {
switch (credentials.credentialsType) {
case DeviceCredentialsType.ACCESS_TOKEN:
this.updateAccessTokenCredentials(credentials, security);
break;
case DeviceCredentialsType.MQTT_BASIC:
this.updateMqttBasicCredentials(credentials);
break;
case DeviceCredentialsType.X509_CERTIFICATE:
break;
}
}
private updateAccessTokenCredentials(credentials: DeviceCredentials, security: GatewayConfigSecurity): void {
this.basicFormGroup.get('thingsboard.security.accessToken').setValue(credentials.credentialsId, { emitEvent: false });
if (security.type === SecurityTypes.TLS_ACCESS_TOKEN) {
this.basicFormGroup.get('thingsboard.security.caCert').setValue(security.caCert, { emitEvent: false });
}
}
private updateMqttBasicCredentials(credentials: DeviceCredentials): void {
const parsedValue = JSON.parse(credentials.credentialsValue);
this.basicFormGroup.get('thingsboard.security.clientId').setValue(parsedValue.clientId, { emitEvent: false });
this.basicFormGroup.get('thingsboard.security.username').setValue(parsedValue.userName, { emitEvent: false });
this.basicFormGroup.get('thingsboard.security.password').setValue(parsedValue.password, { emitEvent: false });
}
}

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

@ -69,8 +69,8 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
constructor(private fb: FormBuilder,
private attributeService: AttributeService,
private deviceService: DeviceService,
private cd: ChangeDetectorRef,
private dialog: MatDialog) {
private cd: ChangeDetectorRef
) {
this.gatewayConfigGroup = this.fb.group({
basicConfig: [],
@ -361,7 +361,7 @@ export class GatewayConfigurationComponent implements AfterViewInit, OnDestroy {
}
private updateFormControls(thingsboard: GatewayGeneralConfig): void {
const { type, accessToken, ...securityConfig } = thingsboard.security || {};
const { type, accessToken, ...securityConfig } = thingsboard.security ?? {};
this.initialCredentials = {
deviceId: this.device as DeviceId,

3
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.html

@ -30,8 +30,7 @@
<tb-js-func #tagFunctionComponent
minHeight="300px"
formControlName="tagFunction"
[objectHighlightRules]="objectHighlightRules"
[propertyHighlightRules]="propertyHighlightRules"
[highlightRules]="highlightRules"
[editorCompleter]="completer"
[functionArgs]="tagFunctionArgs"
[helpId]="tagFunctionHelpId">

16
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata-tag-function-panel.component.ts

@ -28,12 +28,10 @@ import { TbPopoverComponent } from '@shared/components/popover.component';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { WidgetService } from '@core/http/widget.service';
import { TbEditorCompleter } from '@shared/models/ace/completion.models';
import { TbHighlightRule } from '@shared/models/ace/ace.models';
import { AceHighlightRules } from '@shared/models/ace/ace.models';
import {
scadaSymbolClickActionHighlightRules,
scadaSymbolClickActionPropertiesHighlightRules,
scadaSymbolElementStateRenderHighlightRules,
scadaSymbolElementStateRenderPropertiesHighlightRules
scadaSymbolRenderFunctionHighlightRules
} from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { JsFuncComponent } from '@shared/components/js-func.component';
@ -79,9 +77,7 @@ export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, Aft
tagFunctionHelpId: string;
objectHighlightRules: TbHighlightRule[];
propertyHighlightRules: TbHighlightRule[];
highlightRules: AceHighlightRules;
constructor(private fb: UntypedFormBuilder,
private widgetService: WidgetService) {
@ -99,14 +95,12 @@ export class ScadaSymbolMetadataTagFunctionPanelComponent implements OnInit, Aft
if (this.tagFunctionType === 'renderFunction') {
this.panelTitle = 'scada.state-render-function';
this.tagFunctionArgs = ['ctx', 'element'];
this.objectHighlightRules = scadaSymbolElementStateRenderHighlightRules;
this.propertyHighlightRules = scadaSymbolElementStateRenderPropertiesHighlightRules;
this.highlightRules = scadaSymbolRenderFunctionHighlightRules;
this.tagFunctionHelpId = 'scada/tag_state_render_fn';
} else if (this.tagFunctionType === 'clickAction') {
this.panelTitle = 'scada.tag.on-click-action';
this.tagFunctionArgs = ['ctx', 'element', 'event'];
this.objectHighlightRules = scadaSymbolClickActionHighlightRules;
this.propertyHighlightRules = scadaSymbolClickActionPropertiesHighlightRules;
this.highlightRules = scadaSymbolClickActionHighlightRules;
this.tagFunctionHelpId = 'scada/tag_click_action_fn';
}
}

3
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.html

@ -80,8 +80,7 @@
<tb-js-func formControlName="stateRenderFunction"
minHeight="300px"
[editorCompleter]="generalStateRenderFunctionCompleter"
[objectHighlightRules]="scadaSymbolGeneralStateRenderHighlightRules"
[propertyHighlightRules]="scadaSymbolGeneralStateRenderPropertiesHighlightRules"
[highlightRules]="highlightRules"
[functionArgs]="['ctx', 'svg']"
helpId="scada/symbol_state_render_fn">
</tb-js-func>

7
ui-ngx/src/app/modules/home/pages/scada-symbol/metadata-components/scada-symbol-metadata.component.ts

@ -49,8 +49,7 @@ import {
elementStateRenderFunctionCompletions,
generalStateRenderFunctionCompletions,
scadaSymbolContextCompletion,
scadaSymbolGeneralStateRenderHighlightRules,
scadaSymbolGeneralStateRenderPropertiesHighlightRules
scadaSymbolGeneralStateHighlightRules
} from '@home/pages/scada-symbol/scada-symbol-editor.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { IAliasController } from '@core/api/widget-api.models';
@ -122,9 +121,7 @@ export class ScadaSymbolMetadataComponent extends PageComponent implements OnIni
elementStateRenderFunctionCompleter: TbEditorCompleter;
clickActionFunctionCompleter: TbEditorCompleter;
scadaSymbolGeneralStateRenderHighlightRules = scadaSymbolGeneralStateRenderHighlightRules;
scadaSymbolGeneralStateRenderPropertiesHighlightRules = scadaSymbolGeneralStateRenderPropertiesHighlightRules;
highlightRules = scadaSymbolGeneralStateHighlightRules;
constructor(protected store: Store<AppState>,
private fb: UntypedFormBuilder,

253
ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts

@ -34,7 +34,7 @@ import {
} from '@home/components/widget/lib/scada/scada-symbol.models';
import { TbEditorCompletion, TbEditorCompletions } from '@shared/models/ace/completion.models';
import { CustomTranslatePipe } from '@shared/pipe/custom-translate.pipe';
import { TbHighlightRule } from '@shared/models/ace/ace.models';
import { AceHighlightRule, AceHighlightRules } from '@shared/models/ace/ace.models';
import { ValueType } from '@shared/models/constants';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import TooltipPositioningSide = JQueryTooltipster.TooltipPositioningSide;
@ -916,91 +916,184 @@ export class ScadaSymbolElement {
}
const scadaSymbolCtxObjectHighlightRules: TbHighlightRule[] = [
{
class: 'scada-symbol-ctx',
regex: /(?<=\W|^)(ctx)(?=\W|$)\b/
}
];
const identifierRe = /[a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*/;
export const scadaSymbolGeneralStateRenderHighlightRules: TbHighlightRule[] =
scadaSymbolCtxObjectHighlightRules.concat({
class: 'scada-symbol-svg',
regex: /(?<=\W|^)(svg)(?=\W|$)\b/
});
const dotOperatorHighlightRule: AceHighlightRule = {
token: 'punctuation.operator',
regex: /[.](?![.])/,
};
export const scadaSymbolElementStateRenderHighlightRules: TbHighlightRule[] =
scadaSymbolCtxObjectHighlightRules.concat({
class: 'scada-symbol-element',
regex: /(?<=\W|^)(element)(?=\W|$)\b/
});
const endGroupHighlightRule: AceHighlightRule = {
regex: '',
token: 'empty',
next: 'no_regex'
};
export const scadaSymbolClickActionHighlightRules: TbHighlightRule[] =
scadaSymbolElementStateRenderHighlightRules.concat({
class: 'scada-symbol-event',
regex: /(?<=\W|^)(event)(?=\W|$)\b/
});
const scadaSymbolCtxObjectHighlightRule: AceHighlightRule = {
token: 'tb.scada-symbol-ctx',
regex: /\bctx\b/,
next: 'scadaSymbolCtxApi'
};
const scadaSymbolCtxPropertyHighlightRules: TbHighlightRule[] = [
{
class: 'scada-symbol-ctx-properties',
regex: /(?<=ctx\.)(properties)\b/
},
{
class: 'scada-symbol-ctx-tags',
regex: /(?<=ctx\.)(tags)\b/
},
{
class: 'scada-symbol-ctx-values',
regex: /(?<=ctx\.)(values)\b/
},
{
class: 'scada-symbol-ctx-api',
regex: /(?<=ctx\.)(api)\b/
},
{
class: 'scada-symbol-ctx-svg',
regex: /(?<=ctx\.)(svg)\b/
},
{
class: 'scada-symbol-ctx-property',
regex: /(?<=ctx\.properties\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
},
{
class: 'scada-symbol-ctx-tag',
regex: /(?<=ctx\.tags\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
},
{
class: 'scada-symbol-ctx-value',
regex: /(?<=ctx\.values\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
},
{
class: 'scada-symbol-ctx-api-method',
regex: /(?<=ctx\.api\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
},
{
class: 'scada-symbol-ctx-svg-method',
regex: /(?<=ctx\.svg\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
}
];
export const scadaSymbolGeneralStateRenderPropertiesHighlightRules: TbHighlightRule[] =
scadaSymbolCtxPropertyHighlightRules.concat({
class: 'scada-symbol-svg-properties',
regex: /(?<=svg\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
});
const scadaSymbolSVGHighlightRule: AceHighlightRule = {
token: 'tb.scada-symbol-svg',
regex: /\bsvg\b/,
next: 'scadaSymbolSVGApi'
};
export const scadaSymbolElementStateRenderPropertiesHighlightRules: TbHighlightRule[] =
scadaSymbolCtxPropertyHighlightRules.concat({
class: 'scada-symbol-element-properties',
regex: /(?<=element\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
});
const scadaSymbolElementHighlightRule: AceHighlightRule = {
token: 'tb.scada-symbol-element',
regex: /\belement\b/,
next: 'scadaSymbolElementApi'
};
export const scadaSymbolClickActionPropertiesHighlightRules: TbHighlightRule[] =
scadaSymbolElementStateRenderPropertiesHighlightRules.concat({
class: 'scada-symbol-event-properties',
regex: /(?<=event\.)([a-zA-Z$_\u00a1-\uffff][a-zA-Z\d$_\u00a1-\uffff]*)\b/
});
const scadaSymbolEventHighlightRule: AceHighlightRule = {
token: 'tb.scada-symbol-event',
regex: /\bevent\b/,
next: 'scadaSymbolEventApi'
};
const scadaSymbolCtxApiHighlightRules: AceHighlightRules = {
scadaSymbolCtxApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-properties',
regex: /properties/,
next: 'scadaSymbolCtxPropertiesApi'
},
{
token: 'tb.scada-symbol-ctx-tags',
regex: /tags/,
next: 'scadaSymbolCtxTagsApi'
},
{
token: 'tb.scada-symbol-ctx-values',
regex: /values/,
next: 'scadaSymbolCtxValuesApi'
},
{
token: 'tb.scada-symbol-ctx-api',
regex: /api/,
next: 'scadaSymbolCtxApiMethodApi'
},
{
token: 'tb.scada-symbol-ctx-svg',
regex: /svg/,
next: 'scadaSymbolCtxSVGMethodApi'
},
endGroupHighlightRule
],
scadaSymbolCtxPropertiesApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-property',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
],
scadaSymbolCtxTagsApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-tag',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
],
scadaSymbolCtxValuesApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-value',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
],
scadaSymbolCtxApiMethodApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-api-method',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
],
scadaSymbolCtxSVGMethodApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-ctx-svg-method',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
]
};
const scadaSymbolSVGPropertyHighlightRules: AceHighlightRules = {
scadaSymbolSVGApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-svg-properties',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
]
};
const scadaSymbolElementPropertyHighlightRules: AceHighlightRules = {
scadaSymbolElementApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-element-properties',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
]
};
const scadaSymbolEventPropertyHighlightRules: AceHighlightRules = {
scadaSymbolEventApi: [
dotOperatorHighlightRule,
{
token: 'tb.scada-symbol-event-properties',
regex: identifierRe,
next: 'no_regex'
},
endGroupHighlightRule
]
};
export const scadaSymbolGeneralStateHighlightRules: AceHighlightRules = {
start: [
scadaSymbolCtxObjectHighlightRule,
scadaSymbolSVGHighlightRule
],
...scadaSymbolCtxApiHighlightRules,
...scadaSymbolSVGPropertyHighlightRules
};
export const scadaSymbolRenderFunctionHighlightRules: AceHighlightRules = {
no_regex: [
scadaSymbolCtxObjectHighlightRule,
scadaSymbolElementHighlightRule
],
...scadaSymbolCtxApiHighlightRules,
...scadaSymbolElementPropertyHighlightRules
};
export const scadaSymbolClickActionHighlightRules: AceHighlightRules = {
start: [
scadaSymbolCtxObjectHighlightRule,
scadaSymbolElementHighlightRule,
scadaSymbolEventHighlightRule
],
...scadaSymbolCtxApiHighlightRules,
...scadaSymbolElementPropertyHighlightRules,
...scadaSymbolEventPropertyHighlightRules
};
export const generalStateRenderFunctionCompletions = (ctxCompletion: TbEditorCompletion): TbEditorCompletions => ({
ctx: ctxCompletion,

35
ui-ngx/src/app/shared/components/js-func.component.ts

@ -25,15 +25,15 @@ import {
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { ControlValueAccessor, UntypedFormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms';
import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormControl, Validator } from '@angular/forms';
import { Ace } from 'ace-builds';
import { getAce, Range, TbHighlightRule } from '@shared/models/ace/ace.models';
import { AceHighlightRules, getAce, Range } from '@shared/models/ace/ace.models';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { ActionNotificationHide, ActionNotificationShow } from '@core/notification/notification.actions';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UtilsService } from '@core/services/utils.service';
import { deepClone, guid, isUndefined } from '@app/core/utils';
import { guid, isUndefined } from '@app/core/utils';
import { TranslateService } from '@ngx-translate/core';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer';
@ -90,9 +90,7 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
@Input() editorCompleter: TbEditorCompleter;
@Input() propertyHighlightRules: TbHighlightRule[];
@Input() objectHighlightRules: TbHighlightRule[];
@Input() highlightRules: AceHighlightRules;
@Input() globalVariables: Array<string>;
@ -220,27 +218,16 @@ export class JsFuncComponent implements OnInit, OnDestroy, ControlValueAccessor,
});
}
// @ts-ignore
if ((this.propertyHighlightRules?.length || this.objectHighlightRules?.length) && !!this.jsEditor.session.$mode) {
if (!!this.highlightRules && !!this.jsEditor.session.$mode) {
// @ts-ignore
const newMode = new this.jsEditor.session.$mode.constructor();
newMode.$highlightRules = new newMode.HighlightRules();
if (this.propertyHighlightRules?.length) {
const propertiesRules: { token: string; regex: RegExp }[] = newMode.$highlightRules.$rules.property;
const index = propertiesRules.findIndex(p => p.token === 'support.constant');
const additionalPropertyRules: { token: string; regex: RegExp }[] = this.propertyHighlightRules.map(r => ({
token: `tb.${r.class}`,
regex: r.regex
}));
propertiesRules.splice(index, 0, ...additionalPropertyRules);
}
if (this.objectHighlightRules?.length) {
const noRegexRules: { token: string; regex: RegExp }[] = newMode.$highlightRules.$rules.no_regex;
const index = noRegexRules.findIndex(p => Array.isArray(p.token) && p.token[0] === 'support.constant');
const additionalNoRegexRules: { token: string; regex: RegExp }[] = this.objectHighlightRules.map(r => ({
token: `tb.${r.class}`,
regex: r.regex
}));
noRegexRules.splice(index, 0, ...additionalNoRegexRules);
for(const group in this.highlightRules) {
if(!!newMode.$highlightRules.$rules[group]) {
newMode.$highlightRules.$rules[group].unshift(...this.highlightRules[group]);
} else {
newMode.$highlightRules.$rules[group] = this.highlightRules[group];
}
}
// @ts-ignore
this.jsEditor.session.$onChangeMode(newMode);

13
ui-ngx/src/app/shared/models/ace/ace.models.ts

@ -352,8 +352,15 @@ export class Range implements Ace.Range {
}
export interface TbHighlightRule {
class: string;
regex: RegExp;
export interface AceHighlightRules {
[group: string]: Array<AceHighlightRule>;
}
export interface AceHighlightRule {
regex: RegExp | string;
token: string;
next?: string;
}

1
ui-ngx/src/app/shared/models/dashboard.models.ts

@ -97,7 +97,6 @@ export interface GridSettings {
mobileRowHeight?: number;
mobileDisplayLayoutFirst?: boolean;
layoutDimension?: LayoutDimension;
[key: string]: any;
}
export interface DashboardLayout {

Loading…
Cancel
Save