Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard

pull/5496/head
Igor Kulikov 5 years ago
parent
commit
4991bb725d
  1. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java
  2. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java
  3. 223
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java
  4. 3
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  5. 2
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  6. 49
      ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html
  7. 49
      ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html
  8. 60
      ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.html
  9. 32
      ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.scss
  10. 105
      ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.ts
  11. 46
      ui-ngx/src/app/modules/home/components/widget/legend-config.component.html
  12. 189
      ui-ngx/src/app/modules/home/components/widget/legend-config.component.ts
  13. 697
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  14. 58
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss
  15. 37
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts
  16. 1
      ui-ngx/src/app/modules/home/components/widget/widget.component.ts
  17. 1
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  18. 9
      ui-ngx/src/app/shared/components/marked-options.service.ts
  19. 18
      ui-ngx/src/app/shared/components/material-icon-select.component.ts
  20. 43
      ui-ngx/src/app/shared/components/protobuf-content.component.html
  21. 57
      ui-ngx/src/app/shared/components/protobuf-content.component.scss
  22. 193
      ui-ngx/src/app/shared/components/protobuf-content.component.ts
  23. 4
      ui-ngx/src/app/shared/models/ace/ace.models.ts
  24. 7
      ui-ngx/src/app/shared/shared.module.ts
  25. 6
      ui-ngx/src/assets/locale/locale.constant-el_GR.json
  26. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  27. 4
      ui-ngx/src/assets/locale/locale.constant-fr_FR.json
  28. 2
      ui-ngx/src/assets/locale/locale.constant-ka_GE.json
  29. 4
      ui-ngx/src/assets/locale/locale.constant-lv_LV.json
  30. 12
      ui-ngx/src/assets/locale/locale.constant-ru_RU.json
  31. 12
      ui-ngx/src/assets/locale/locale.constant-uk_UA.json

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java

@ -182,7 +182,8 @@ public class TbHttpClient {
HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
HttpEntity<String> entity; HttpEntity<String> entity;
if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) || if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) ||
HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method)) { HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) ||
config.isIgnoreRequestBody()) {
entity = new HttpEntity<>(headers); entity = new HttpEntity<>(headers);
} else { } else {
entity = new HttpEntity<>(msg.getData(), headers); entity = new HttpEntity<>(msg.getData(), headers);

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeConfiguration.java

@ -47,6 +47,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
private String proxyPassword; private String proxyPassword;
private String proxyScheme; private String proxyScheme;
private ClientCredentials credentials; private ClientCredentials credentials;
private boolean ignoreRequestBody;
@Override @Override
public TbRestApiCallNodeConfiguration defaultConfiguration() { public TbRestApiCallNodeConfiguration defaultConfiguration() {
@ -61,6 +62,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
configuration.setTrimQueue(false); configuration.setTrimQueue(false);
configuration.setEnableProxy(false); configuration.setEnableProxy(false);
configuration.setCredentials(new AnonymousCredentials()); configuration.setCredentials(new AnonymousCredentials());
configuration.setIgnoreRequestBody(false);
return configuration; return configuration;
} }

223
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/rest/TbRestApiCallNodeTest.java

@ -0,0 +1,223 @@
/**
* Copyright © 2016-2021 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.rule.engine.rest;
import static org.junit.Assert.*;
import static org.mockito.Mockito.verify;
import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.http.Header;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.config.SocketConfig;
import org.apache.http.impl.bootstrap.HttpServer;
import org.apache.http.impl.bootstrap.ServerBootstrap;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestHandler;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbEmail;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.ObjectMapper;
@RunWith(MockitoJUnitRunner.class)
public class TbRestApiCallNodeTest {
private TbRestApiCallNode restNode;
@Mock
private TbContext ctx;
private EntityId originator = new DeviceId(Uuids.timeBased());
private TbMsgMetaData metaData = new TbMsgMetaData();
private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased());
private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
private HttpServer server;
public void setupServer(String pattern, HttpRequestHandler handler) throws IOException {
SocketConfig config = SocketConfig.custom().setSoReuseAddress(true).setTcpNoDelay(true).build();
server = ServerBootstrap.bootstrap()
.setSocketConfig(config)
.registerHandler(pattern, handler)
.create();
server.start();
}
private void initWithConfig(TbRestApiCallNodeConfiguration config) {
try {
ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
restNode = new TbRestApiCallNode();
restNode.init(ctx, nodeConfiguration);
} catch (TbNodeException ex) {
throw new IllegalStateException(ex);
}
}
@After
public void teardown() {
server.stop();
}
@Test
public void deleteRequestWithoutBody() throws IOException, InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final String path = "/path/to/delete";
setupServer("*", new HttpRequestHandler() {
@Override
public void handle(HttpRequest request, HttpResponse response, HttpContext context)
throws HttpException, IOException {
try {
assertEquals("Request path matches", request.getRequestLine().getUri(), path);
assertFalse("Content-Type not included", request.containsHeader("Content-Type"));
assertTrue("Custom header included", request.containsHeader("Foo"));
assertEquals("Custom header value", "Bar", request.getFirstHeader("Foo").getValue());
response.setStatusCode(200);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
// ignore
} finally {
latch.countDown();
}
}
}).start();
} catch ( Exception e ) {
System.out.println("Exception handling request: " + e.toString());
e.printStackTrace();
latch.countDown();
}
}
});
TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
config.setRequestMethod("DELETE");
config.setHeaders(Collections.singletonMap("Foo", "Bar"));
config.setIgnoreRequestBody(true);
config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path));
initWithConfig(config);
TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
restNode.onMsg(ctx, msg);
assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS));
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
assertEquals("USER", typeCaptor.getValue());
assertEquals(originator, originatorCaptor.getValue());
assertNotSame(metaData, metadataCaptor.getValue());
assertEquals("{}", dataCaptor.getValue());
}
@Test
public void deleteRequestWithBody() throws IOException, InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final String path = "/path/to/delete";
setupServer("*", new HttpRequestHandler() {
@Override
public void handle(HttpRequest request, HttpResponse response, HttpContext context)
throws HttpException, IOException {
try {
assertEquals("Request path matches", path, request.getRequestLine().getUri());
assertTrue("Content-Type included", request.containsHeader("Content-Type"));
assertEquals("Content-Type value", "text/plain;charset=ISO-8859-1",
request.getFirstHeader("Content-Type").getValue());
assertTrue("Content-Length included", request.containsHeader("Content-Length"));
assertEquals("Content-Length value", "2",
request.getFirstHeader("Content-Length").getValue());
assertTrue("Custom header included", request.containsHeader("Foo"));
assertEquals("Custom header value", "Bar", request.getFirstHeader("Foo").getValue());
response.setStatusCode(200);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
// ignore
} finally {
latch.countDown();
}
}
}).start();
} catch ( Exception e ) {
System.out.println("Exception handling request: " + e.toString());
e.printStackTrace();
latch.countDown();
}
}
});
TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
config.setRequestMethod("DELETE");
config.setHeaders(Collections.singletonMap("Foo", "Bar"));
config.setIgnoreRequestBody(false);
config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path));
initWithConfig(config);
TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
restNode.onMsg(ctx, msg);
assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS));
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
assertEquals("USER", typeCaptor.getValue());
assertEquals(originator, originatorCaptor.getValue());
assertNotSame(metaData, metadataCaptor.getValue());
assertEquals("{}", dataCaptor.getValue());
}
}

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

@ -15,7 +15,7 @@
limitations under the License. limitations under the License.
--> -->
<div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}" tb-toast toastTarget="dashboardRoot" <div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}"
fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
<tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
<section class="tb-dashboard-toolbar" <section class="tb-dashboard-toolbar"
@ -139,6 +139,7 @@
</tb-dashboard-toolbar> </tb-dashboard-toolbar>
</section> </section>
<section class="tb-dashboard-container tb-absolute-fill" <section class="tb-dashboard-container tb-absolute-fill"
tb-toast toastTarget="dashboardRoot"
#dashboardContainer #dashboardContainer
[ngClass]="{ 'is-fullscreen': forceFullscreen, [ngClass]="{ 'is-fullscreen': forceFullscreen,
'tb-dashboard-toolbar-opened': toolbarOpened, 'tb-dashboard-toolbar-opened': toolbarOpened,

2
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -49,7 +49,6 @@ import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-
import { DataKeysComponent } from '@home/components/widget/data-keys.component'; import { DataKeysComponent } from '@home/components/widget/data-keys.component';
import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component'; import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component';
import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
import { LegendConfigPanelComponent } from '@home/components/widget/legend-config-panel.component';
import { LegendConfigComponent } from '@home/components/widget/legend-config.component'; import { LegendConfigComponent } from '@home/components/widget/legend-config.component';
import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component'; import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component';
import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component';
@ -182,7 +181,6 @@ import { DeviceProfileCommonModule } from '@home/components/profile/device/commo
DataKeysComponent, DataKeysComponent,
DataKeyConfigComponent, DataKeyConfigComponent,
DataKeyConfigDialogComponent, DataKeyConfigDialogComponent,
LegendConfigPanelComponent,
LegendConfigComponent, LegendConfigComponent,
ManageWidgetActionsComponent, ManageWidgetActionsComponent,
WidgetActionDialogComponent, WidgetActionDialogComponent,

49
ui-ngx/src/app/modules/home/components/profile/device/coap-device-profile-transport-configuration.component.html

@ -47,35 +47,50 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<div *ngIf="protoPayloadType" fxLayout="column"> <div *ngIf="protoPayloadType" fxLayout="column">
<mat-form-field fxFlex> <ng-container>
<mat-label translate>device-profile.telemetry-proto-schema</mat-label> <tb-protobuf-content
<textarea matInput required formControlName="deviceTelemetryProtoSchema" rows="5"></textarea> fxFlex
formControlName="deviceTelemetryProtoSchema"
label="{{ 'device-profile.telemetry-proto-schema' | translate }}"
[fillHeight]="true">
</tb-protobuf-content>
<mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')"> <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
{{ 'device-profile.telemetry-proto-schema-required' | translate}} {{ 'device-profile.telemetry-proto-schema-required' | translate}}
</mat-error> </mat-error>
</mat-form-field> </ng-container>
<mat-form-field fxFlex> <ng-container>
<mat-label translate>device-profile.attributes-proto-schema</mat-label> <tb-protobuf-content
<textarea matInput required formControlName="deviceAttributesProtoSchema" rows="5"></textarea> fxFlex
formControlName="deviceAttributesProtoSchema"
label="{{ 'device-profile.attributes-proto-schema' | translate }}"
[fillHeight]="true">
</tb-protobuf-content>
<mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')"> <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
{{ 'device-profile.attributes-proto-schema-required' | translate}} {{ 'device-profile.attributes-proto-schema-required' | translate}}
</mat-error> </mat-error>
</mat-form-field> </ng-container>
<mat-form-field style="padding-bottom: 20px" fxFlex> <ng-container>
<mat-label translate>device-profile.rpc-request-proto-schema</mat-label> <tb-protobuf-content
<textarea matInput required formControlName="deviceRpcRequestProtoSchema" rows="5"></textarea> fxFlex
formControlName="deviceRpcRequestProtoSchema"
label="{{ 'device-profile.rpc-request-proto-schema' | translate }}"
[fillHeight]="true">
</tb-protobuf-content>
<mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')"> <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
{{ 'device-profile.rpc-request-proto-schema-required' | translate}} {{ 'device-profile.rpc-request-proto-schema-required' | translate}}
</mat-error> </mat-error>
<mat-hint class="tb-hint" translate>device-profile.rpc-request-proto-schema-hint</mat-hint> </ng-container>
</mat-form-field> <ng-container>
<mat-form-field fxFlex> <tb-protobuf-content
<mat-label translate>device-profile.rpc-response-proto-schema</mat-label> fxFlex
<textarea matInput required formControlName="deviceRpcResponseProtoSchema" rows="5"></textarea> formControlName="deviceRpcResponseProtoSchema"
label="{{ 'device-profile.rpc-response-proto-schema' | translate }}"
[fillHeight]="true">
</tb-protobuf-content>
<mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')"> <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
{{ 'device-profile.rpc-response-proto-schema-required' | translate}} {{ 'device-profile.rpc-response-proto-schema-required' | translate}}
</mat-error> </mat-error>
</mat-form-field> </ng-container>
</div> </div>
</div> </div>
</fieldset> </fieldset>

49
ui-ngx/src/app/modules/home/components/profile/device/mqtt-device-profile-transport-configuration.component.html

@ -86,35 +86,50 @@
</div> </div>
</div> </div>
<div *ngIf="protoPayloadType" fxLayout="column"> <div *ngIf="protoPayloadType" fxLayout="column">
<mat-form-field fxFlex> <ng-container>
<mat-label translate>device-profile.telemetry-proto-schema</mat-label> <tb-protobuf-content
<textarea matInput required formControlName="deviceTelemetryProtoSchema" rows="5"></textarea> fxFlex
formControlName="deviceTelemetryProtoSchema"
label="{{ 'device-profile.telemetry-proto-schema' | translate }}"
[fillHeight]="true">
</tb-protobuf-content>
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')"> <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
{{ 'device-profile.telemetry-proto-schema-required' | translate}} {{ 'device-profile.telemetry-proto-schema-required' | translate}}
</mat-error> </mat-error>
</mat-form-field> </ng-container>
<mat-form-field fxFlex> <ng-container>
<mat-label translate>device-profile.attributes-proto-schema</mat-label> <tb-protobuf-content
<textarea matInput required formControlName="deviceAttributesProtoSchema" rows="5"></textarea> fxFlex
formControlName="deviceAttributesProtoSchema"
label="{{ 'device-profile.attributes-proto-schema' | translate }}"
[fillHeight]="true">
</tb-protobuf-content>
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')"> <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
{{ 'device-profile.attributes-proto-schema-required' | translate}} {{ 'device-profile.attributes-proto-schema-required' | translate}}
</mat-error> </mat-error>
</mat-form-field> </ng-container>
<mat-form-field style="padding-bottom: 20px" fxFlex> <ng-container>
<mat-label translate>device-profile.rpc-request-proto-schema</mat-label> <tb-protobuf-content
<textarea matInput required formControlName="deviceRpcRequestProtoSchema" rows="5"></textarea> fxFlex
formControlName="deviceRpcRequestProtoSchema"
label="{{ 'device-profile.rpc-request-proto-schema' | translate }}"
[fillHeight]="true">
</tb-protobuf-content>
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')"> <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
{{ 'device-profile.rpc-request-proto-schema-required' | translate}} {{ 'device-profile.rpc-request-proto-schema-required' | translate}}
</mat-error> </mat-error>
<mat-hint class="tb-hint" translate>device-profile.rpc-request-proto-schema-hint</mat-hint> </ng-container>
</mat-form-field> <ng-container>
<mat-form-field fxFlex> <tb-protobuf-content
<mat-label translate>device-profile.rpc-response-proto-schema</mat-label> fxFlex
<textarea matInput required formControlName="deviceRpcResponseProtoSchema" rows="5"></textarea> formControlName="deviceRpcResponseProtoSchema"
label="{{ 'device-profile.rpc-response-proto-schema' | translate }}"
[fillHeight]="true">
</tb-protobuf-content>
<mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')"> <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
{{ 'device-profile.rpc-response-proto-schema-required' | translate}} {{ 'device-profile.rpc-response-proto-schema-required' | translate}}
</mat-error> </mat-error>
</mat-form-field> </ng-container>
</div> </div>
</div> </div>
</fieldset> </fieldset>

60
ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.html

@ -1,60 +0,0 @@
<!--
Copyright © 2016-2021 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.
-->
<form [formGroup]="legendConfigForm">
<fieldset [disabled]="(isLoading$ | async)">
<div class="mat-content" style="height: 100%;">
<div class="mat-padding">
<section fxLayout="column">
<mat-form-field>
<mat-label translate>legend.direction</mat-label>
<mat-select matInput formControlName="direction" style="min-width: 150px;">
<mat-option *ngFor="let direction of legendDirections" [value]="direction">
{{ legendDirectionTranslations.get(legendDirection[direction]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label translate>legend.position</mat-label>
<mat-select matInput formControlName="position" style="min-width: 150px;">
<mat-option *ngFor="let pos of legendPositions" [value]="pos"
[disabled]="legendConfigForm.get('direction').value === legendDirection.row &&
(pos === legendPosition.left || pos === legendPosition.right)">
{{ legendPositionTranslations.get(legendPosition[pos]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-checkbox formControlName="sortDataKeys">
{{ 'legend.sort-legend' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showMin">
{{ 'legend.show-min' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showMax">
{{ 'legend.show-max' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showAvg">
{{ 'legend.show-avg' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showTotal">
{{ 'legend.show-total' | translate }}
</mat-checkbox>
</section>
</div>
</div>
</fieldset>
</form>

32
ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.scss

@ -1,32 +0,0 @@
/**
* Copyright © 2016-2021 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.
*/
:host {
width: 100%;
height: 100%;
form,
fieldset {
height: 100%;
}
.mat-content {
overflow: hidden;
background-color: #fff;
}
.mat-padding {
padding: 16px;
}
}

105
ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.ts

@ -1,105 +0,0 @@
///
/// Copyright © 2016-2021 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.
///
import { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core';
import { OverlayRef } from '@angular/cdk/overlay';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormGroup } from '@angular/forms';
import {
LegendConfig,
LegendDirection,
legendDirectionTranslationMap,
LegendPosition,
legendPositionTranslationMap
} from '@shared/models/widget.models';
export const LEGEND_CONFIG_PANEL_DATA = new InjectionToken<any>('LegendConfigPanelData');
export interface LegendConfigPanelData {
legendConfig: LegendConfig;
legendConfigUpdated: (legendConfig: LegendConfig) => void;
}
@Component({
selector: 'tb-legend-config-panel',
templateUrl: './legend-config-panel.component.html',
styleUrls: ['./legend-config-panel.component.scss']
})
export class LegendConfigPanelComponent extends PageComponent implements OnInit {
legendConfigForm: FormGroup;
legendDirection = LegendDirection;
legendDirections = Object.keys(LegendDirection);
legendDirectionTranslations = legendDirectionTranslationMap;
legendPosition = LegendPosition;
legendPositions = Object.keys(LegendPosition);
legendPositionTranslations = legendPositionTranslationMap;
constructor(@Inject(LEGEND_CONFIG_PANEL_DATA) public data: LegendConfigPanelData,
public overlayRef: OverlayRef,
protected store: Store<AppState>,
public fb: FormBuilder,
public viewContainerRef: ViewContainerRef) {
super(store);
}
ngOnInit(): void {
this.legendConfigForm = this.fb.group({
direction: [this.data.legendConfig.direction, []],
position: [this.data.legendConfig.position, []],
sortDataKeys: [this.data.legendConfig.sortDataKeys, []],
showMin: [this.data.legendConfig.showMin, []],
showMax: [this.data.legendConfig.showMax, []],
showAvg: [this.data.legendConfig.showAvg, []],
showTotal: [this.data.legendConfig.showTotal, []]
});
this.legendConfigForm.get('direction').valueChanges.subscribe((direction: LegendDirection) => {
this.onDirectionChanged(direction);
});
this.onDirectionChanged(this.data.legendConfig.direction);
this.legendConfigForm.valueChanges.subscribe(() => {
this.update();
});
}
private onDirectionChanged(direction: LegendDirection) {
if (direction === LegendDirection.row) {
let position: LegendPosition = this.legendConfigForm.get('position').value;
if (position !== LegendPosition.bottom && position !== LegendPosition.top) {
position = LegendPosition.bottom;
}
this.legendConfigForm.patchValue(
{
position
}, {emitEvent: false}
);
}
}
update() {
const newLegendConfig: LegendConfig = this.legendConfigForm.value;
this.data.legendConfigUpdated(newLegendConfig);
}
}

46
ui-ngx/src/app/modules/home/components/widget/legend-config.component.html

@ -15,9 +15,43 @@
limitations under the License. limitations under the License.
--> -->
<button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled" <form [formGroup]="legendConfigForm">
type="button" <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
mat-button mat-raised-button color="primary" (click)="openEditMode()"> fxLayoutGap="8px">
<mat-icon class="material-icons">toc</mat-icon> <mat-form-field fxFlex>
<span translate>legend.settings</span> <mat-label translate>legend.direction</mat-label>
</button> <mat-select matInput formControlName="direction">
<mat-option *ngFor="let direction of legendDirections" [value]="direction">
{{ legendDirectionTranslations.get(legendDirection[direction]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>legend.position</mat-label>
<mat-select matInput formControlName="position">
<mat-option *ngFor="let pos of legendPositions" [value]="pos"
[disabled]="legendConfigForm.get('direction').value === legendDirection.row &&
(pos === legendPosition.left || pos === legendPosition.right)">
{{ legendPositionTranslations.get(legendPosition[pos]) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row wrap" fxLayoutAlign="space-between center" fxLayoutGap="8px">
<mat-checkbox formControlName="showMin" fxFlex="48">
{{ 'legend.show-min' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showMax" fxFlex="48">
{{ 'legend.show-max' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showAvg" fxFlex="48">
{{ 'legend.show-avg' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="showTotal" fxFlex="48">
{{ 'legend.show-total' | translate }}
</mat-checkbox>
<mat-checkbox formControlName="sortDataKeys" fxFlex="48">
{{ 'legend.sort-legend' | translate }}
</mat-checkbox>
</div>
</form>

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

@ -14,32 +14,17 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { isDefined } from '@core/utils';
import { import {
Component, LegendConfig,
forwardRef, LegendDirection,
Inject, legendDirectionTranslationMap,
Injector, LegendPosition,
Input, legendPositionTranslationMap
OnDestroy, } from '@shared/models/widget.models';
OnInit, import { Subscription } from 'rxjs';
StaticProvider,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { DOCUMENT } from '@angular/common';
import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { MediaBreakpoints } from '@shared/models/constants';
import { BreakpointObserver } from '@angular/cdk/layout';
import { WINDOW } from '@core/services/window.service';
import { deepClone } from '@core/utils';
import { LegendConfig } from '@shared/models/widget.models';
import {
LEGEND_CONFIG_PANEL_DATA,
LegendConfigPanelComponent,
LegendConfigPanelData
} from '@home/components/widget/legend-config-panel.component';
// @dynamic // @dynamic
@Component({ @Component({
@ -58,105 +43,60 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
@Input() disabled: boolean; @Input() disabled: boolean;
@ViewChild('legendConfigPanelOrigin') legendConfigPanelOrigin: CdkOverlayOrigin; legendConfigForm: FormGroup;
legendDirection = LegendDirection;
innerValue: LegendConfig; legendDirections = Object.keys(LegendDirection);
legendDirectionTranslations = legendDirectionTranslationMap;
legendPosition = LegendPosition;
legendPositions = Object.keys(LegendPosition);
legendPositionTranslations = legendPositionTranslationMap;
private legendSettingsFormChanges$: Subscription;
private legendSettingsFormDirectionChanges$: Subscription;
private propagateChange = (_: any) => {}; private propagateChange = (_: any) => {};
constructor(private overlay: Overlay, constructor(private fb: FormBuilder) {
public viewContainerRef: ViewContainerRef,
public breakpointObserver: BreakpointObserver,
@Inject(DOCUMENT) private document: Document,
@Inject(WINDOW) private window: Window) {
} }
ngOnInit(): void { ngOnInit(): void {
this.legendConfigForm = this.fb.group({
direction: [null, []],
position: [null, []],
sortDataKeys: [null, []],
showMin: [null, []],
showMax: [null, []],
showAvg: [null, []],
showTotal: [null, []]
});
this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges
.subscribe((direction: LegendDirection) => {
this.onDirectionChanged(direction);
});
this.legendSettingsFormChanges$ = this.legendConfigForm.valueChanges.subscribe(
() => this.legendConfigUpdated()
);
} }
ngOnDestroy(): void { private onDirectionChanged(direction: LegendDirection) {
} if (direction === LegendDirection.row) {
let position: LegendPosition = this.legendConfigForm.get('position').value;
openEditMode() { if (position !== LegendPosition.bottom && position !== LegendPosition.top) {
if (this.disabled) { position = LegendPosition.bottom;
return;
}
const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);
const position = this.overlay.position();
const config = new OverlayConfig({
panelClass: 'tb-legend-config-panel',
backdropClass: 'cdk-overlay-transparent-backdrop',
hasBackdrop: isGtSm,
});
if (isGtSm) {
config.minWidth = '220px';
config.maxHeight = '300px';
const panelHeight = 220;
const panelWidth = 220;
const el = this.legendConfigPanelOrigin.elementRef.nativeElement;
const offset = el.getBoundingClientRect();
const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;
const bottomY = offset.bottom - scrollTop;
const leftX = offset.left - scrollLeft;
let originX;
let originY;
let overlayX;
let overlayY;
const wHeight = this.document.documentElement.clientHeight;
const wWidth = this.document.documentElement.clientWidth;
if (bottomY + panelHeight > wHeight) {
originY = 'top';
overlayY = 'bottom';
} else {
originY = 'bottom';
overlayY = 'top';
} }
if (leftX + panelWidth > wWidth) { this.legendConfigForm.patchValue({position}, {emitEvent: false}
originX = 'end'; );
overlayX = 'end';
} else {
originX = 'start';
overlayX = 'start';
}
const connectedPosition: ConnectedPosition = {
originX,
originY,
overlayX,
overlayY
};
config.positionStrategy = position.flexibleConnectedTo(this.legendConfigPanelOrigin.elementRef)
.withPositions([connectedPosition]);
} else {
config.minWidth = '100%';
config.minHeight = '100%';
config.positionStrategy = position.global().top('0%').left('0%')
.right('0%').bottom('0%');
} }
const overlayRef = this.overlay.create(config);
overlayRef.backdropClick().subscribe(() => {
overlayRef.dispose();
});
const injector = this._createLegendConfigPanelInjector(
overlayRef,
{
legendConfig: deepClone(this.innerValue),
legendConfigUpdated: this.legendConfigUpdated.bind(this)
}
);
overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector));
} }
private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): Injector { ngOnDestroy(): void {
const providers: StaticProvider[] = [ if (this.legendSettingsFormDirectionChanges$) {
{provide: LEGEND_CONFIG_PANEL_DATA, useValue: data}, this.legendSettingsFormDirectionChanges$.unsubscribe();
{provide: OverlayRef, useValue: overlayRef} this.legendSettingsFormDirectionChanges$ = null;
]; }
return Injector.create({parent: this.viewContainerRef.injector, providers}); if (this.legendSettingsFormChanges$) {
this.legendSettingsFormChanges$.unsubscribe();
this.legendSettingsFormChanges$ = null;
}
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
@ -168,14 +108,29 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
setDisabledState(isDisabled: boolean): void { setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled; this.disabled = isDisabled;
if (this.disabled) {
this.legendConfigForm.disable({emitEvent: false});
} else {
this.legendConfigForm.enable({emitEvent: false});
}
} }
writeValue(obj: LegendConfig): void { writeValue(legendConfig: LegendConfig): void {
this.innerValue = obj; if (legendConfig) {
this.legendConfigForm.patchValue({
direction: legendConfig.direction,
position: legendConfig.position,
sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false,
showMin: isDefined(legendConfig.showMin) ? legendConfig.showMin : false,
showMax: isDefined(legendConfig.showMax) ? legendConfig.showMax : false,
showAvg: isDefined(legendConfig.showAvg) ? legendConfig.showAvg : false,
showTotal: isDefined(legendConfig.showTotal) ? legendConfig.showTotal : false
}, {emitEvent: false});
}
this.onDirectionChanged(legendConfig.direction);
} }
private legendConfigUpdated(legendConfig: LegendConfig) { private legendConfigUpdated() {
this.innerValue = legendConfig; this.propagateChange(this.legendConfigForm.value);
this.propagateChange(this.innerValue);
} }
} }

697
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -82,222 +82,246 @@
</mat-checkbox> </mat-checkbox>
</div> </div>
</div> </div>
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc && <mat-accordion multi>
widgetType !== widgetTypes.alarm && <mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&
modelValue?.isDataEnabled" [expanded]="true"> widgetType !== widgetTypes.alarm &&
<mat-expansion-panel-header> modelValue?.isDataEnabled" [expanded]="true">
<mat-panel-title fxLayout="column"> <mat-expansion-panel-header>
<div class="tb-panel-title" translate>widget-config.datasources</div> <mat-panel-title fxLayout="column">
<div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1" <div class="tb-panel-title" translate>widget-config.datasources</div>
class="tb-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div> <div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1"
</mat-panel-title> class="tb-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div>
</mat-expansion-panel-header> </mat-panel-title>
<div *ngIf="datasourcesFormArray().length === 0; else datasourcesTemplate"> </mat-expansion-panel-header>
<span translate fxLayoutAlign="center center" <div *ngIf="datasourcesFormArray().length === 0; else datasourcesTemplate">
class="tb-prompt">datasource.add-datasource-prompt</span> <span translate fxLayoutAlign="center center"
</div> class="tb-prompt">datasource.add-datasource-prompt</span>
<ng-template #datasourcesTemplate>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<span style="width: 60px;"></span>
<div fxFlex fxLayout="row" fxLayoutAlign="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate style="min-width: 120px;">widget-config.datasource-type</span>
<span fxHide fxShow.gt-sm translate fxFlex
style="padding-left: 10px;">widget-config.datasource-parameters</span>
<span style="min-width: 40px;"></span>
</div>
</div> </div>
<div style="overflow: auto; padding-bottom: 15px;"> <ng-template #datasourcesTemplate>
<mat-list dndDropzone dndEffectAllowed="move" <div fxFlex fxLayout="row" fxLayoutAlign="start center">
(dndDrop)="onDatasourceDrop($event)" <span style="width: 60px;"></span>
[dndDisableIf]="disabled" formArrayName="datasources"> <div fxFlex fxLayout="row" fxLayoutAlign="start center"
<mat-list-item dndPlaceholderRef style="padding: 0 0 0 10px; margin: 5px;">
class="dndPlaceholder"> <span translate style="min-width: 120px;">widget-config.datasource-type</span>
</mat-list-item> <span fxHide fxShow.gt-sm translate fxFlex
<mat-list-item *ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;" style="padding-left: 10px;">widget-config.datasource-parameters</span>
[dndDraggable]="datasourceControl.value" <span style="min-width: 40px;"></span>
(dndMoved)="dndDatasourceMoved($index)" </div>
[dndDisableIf]="disabled" </div>
dndEffectAllowed="move"> <div style="overflow: auto; padding-bottom: 15px;">
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> <mat-list dndDropzone dndEffectAllowed="move"
<div style="width: 60px;"> (dndDrop)="onDatasourceDrop($event)"
<button *ngIf="!disabled" mat-icon-button color="primary" [dndDisableIf]="disabled" formArrayName="datasources">
class="handle" <mat-list-item dndPlaceholderRef
style="min-width: 40px; margin: 0" class="dndPlaceholder">
dndHandle </mat-list-item>
matTooltip="{{ 'action.drag' | translate }}" <mat-list-item *ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;"
matTooltipPosition="above"> [dndDraggable]="datasourceControl.value"
<mat-icon>drag_handle</mat-icon> (dndMoved)="dndDatasourceMoved($index)"
</button> [dndDisableIf]="disabled"
<span>{{$index + 1}}.</span> dndEffectAllowed="move">
</div> <div fxFlex fxLayout="row" fxLayoutAlign="start center">
<div class="mat-elevation-z4" fxFlex <div style="width: 60px;">
fxLayout="row" <button *ngIf="!disabled" mat-icon-button color="primary"
fxLayoutAlign="start center" class="handle"
style="padding: 0 0 0 10px; margin: 5px;"> style="min-width: 40px; margin: 0"
<section fxFlex dndHandle
fxLayout="column" matTooltip="{{ 'action.drag' | translate }}"
fxLayoutAlign="center" matTooltipPosition="above">
fxLayout.gt-sm="row" <mat-icon>drag_handle</mat-icon>
fxLayoutAlign.gt-sm="start center"> </button>
<mat-form-field class="tb-datasource-type"> <span>{{$index + 1}}.</span>
<mat-select [formControl]="datasourceControl.get('type')"> </div>
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType"> <div class="mat-elevation-z4" fxFlex
{{ datasourceTypesTranslations.get(datasourceType) | translate }} fxLayout="row"
</mat-option> fxLayoutAlign="start center"
</mat-select> style="padding: 0 0 0 10px; margin: 5px;">
</mat-form-field> <section fxFlex
<section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value"> fxLayout="column"
<ng-template [ngSwitchCase]="datasourceType.function"> fxLayoutAlign="center"
<mat-form-field floatLabel="always" fxLayout.gt-sm="row"
class="tb-datasource-name" style="min-width: 200px;"> fxLayoutAlign.gt-sm="start center">
<mat-label></mat-label> <mat-form-field class="tb-datasource-type">
<input matInput <mat-select [formControl]="datasourceControl.get('type')">
placeholder="{{ 'datasource.label' | translate }}" <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
[formControl]="datasourceControl.get('name')"> {{ datasourceTypesTranslations.get(datasourceType) | translate }}
</mat-form-field> </mat-option>
</ng-template> </mat-select>
<ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity || </mat-form-field>
datasourceControl.get('type').value === datasourceType.entityCount ? datasourceControl.get('type').value : ''"> <section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">
<tb-entity-alias-select <ng-template [ngSwitchCase]="datasourceType.function">
[showLabel]="true" <mat-form-field floatLabel="always"
[tbRequired]="true" class="tb-datasource-name" style="min-width: 200px;">
[aliasController]="aliasController" <mat-label></mat-label>
[formControl]="datasourceControl.get('entityAliasId')" <input matInput
[callbacks]="widgetConfigCallbacks"> placeholder="{{ 'datasource.label' | translate }}"
</tb-entity-alias-select> [formControl]="datasourceControl.get('name')">
<tb-filter-select </mat-form-field>
[showLabel]="true" </ng-template>
[aliasController]="aliasController" <ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity ||
[formControl]="datasourceControl.get('filterId')" datasourceControl.get('type').value === datasourceType.entityCount ? datasourceControl.get('type').value : ''">
[callbacks]="widgetConfigCallbacks"> <tb-entity-alias-select
</tb-filter-select> [showLabel]="true"
<mat-form-field *ngIf="datasourceControl.get('type').value === datasourceType.entityCount" [tbRequired]="true"
floatLabel="always" [aliasController]="aliasController"
class="tb-datasource-name no-border-top" style="min-width: 200px;"> [formControl]="datasourceControl.get('entityAliasId')"
<mat-label></mat-label> [callbacks]="widgetConfigCallbacks">
<input matInput </tb-entity-alias-select>
placeholder="{{ 'datasource.label' | translate }}" <tb-filter-select
[formControl]="datasourceControl.get('name')"> [showLabel]="true"
</mat-form-field> [aliasController]="aliasController"
</ng-template> [formControl]="datasourceControl.get('filterId')"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
<mat-form-field *ngIf="datasourceControl.get('type').value === datasourceType.entityCount"
floatLabel="always"
class="tb-datasource-name no-border-top" style="min-width: 200px;">
<mat-label></mat-label>
<input matInput
placeholder="{{ 'datasource.label' | translate }}"
[formControl]="datasourceControl.get('name')">
</mat-form-field>
</ng-template>
</section>
<tb-data-keys class="tb-data-keys" fxFlex
[widgetType]="widgetType"
[datasourceType]="datasourceControl.get('type').value"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
[optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"
[aliasController]="aliasController"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
[callbacks]="widgetConfigCallbacks"
[entityAliasId]="datasourceControl.get('entityAliasId').value"
[formControl]="datasourceControl.get('dataKeys')">
</tb-data-keys>
</section> </section>
<tb-data-keys class="tb-data-keys" fxFlex <button [disabled]="isLoading$ | async"
[widgetType]="widgetType" type="button"
[datasourceType]="datasourceControl.get('type').value" mat-icon-button color="primary"
[maxDataKeys]="modelValue?.typeParameters?.maxDataKeys" style="min-width: 40px;"
[optDataKeys]="modelValue?.typeParameters?.dataKeysOptional" (click)="removeDatasource($index)"
[aliasController]="aliasController" matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" matTooltipPosition="above">
[callbacks]="widgetConfigCallbacks" <mat-icon>close</mat-icon>
[entityAliasId]="datasourceControl.get('entityAliasId').value" </button>
[formControl]="datasourceControl.get('dataKeys')"> </div>
</tb-data-keys>
</section>
<button [disabled]="isLoading$ | async"
type="button"
mat-icon-button color="primary"
style="min-width: 40px;"
(click)="removeDatasource($index)"
matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>close</mat-icon>
</button>
</div> </div>
</div> </mat-list-item>
</mat-list-item> </mat-list>
</mat-list> </div>
</ng-template>
<div fxFlex fxLayout="row" fxLayoutAlign="start center">
<button [disabled]="isLoading$ | async"
type="button"
mat-raised-button color="primary"
[fxShow]="modelValue?.typeParameters &&
(modelValue?.typeParameters.maxDatasources == -1 || datasourcesFormArray().controls.length < modelValue?.typeParameters.maxDatasources)"
(click)="addDatasource()"
matTooltip="{{ 'widget-config.add-datasource' | translate }}"
matTooltipPosition="above">
<mat-icon>add</mat-icon>
<span translate>action.add</span>
</button>
</div> </div>
</ng-template> </mat-expansion-panel>
<div fxFlex fxLayout="row" fxLayoutAlign="start center"> <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc &&
<button [disabled]="isLoading$ | async" modelValue?.isDataEnabled" [expanded]="true">
type="button" <mat-expansion-panel-header>
mat-raised-button color="primary" <mat-panel-title>
[fxShow]="modelValue?.typeParameters && {{ 'widget-config.target-device' | translate }}
(modelValue?.typeParameters.maxDatasources == -1 || datasourcesFormArray().controls.length < modelValue?.typeParameters.maxDatasources)" </mat-panel-title>
(click)="addDatasource()" </mat-expansion-panel-header>
matTooltip="{{ 'widget-config.add-datasource' | translate }}" <div [formGroup]="targetDeviceSettings" style="padding: 0 5px;">
matTooltipPosition="above"> <tb-entity-alias-select fxFlex
<mat-icon>add</mat-icon> [tbRequired]="!widgetEditMode"
<span translate>action.add</span> [aliasController]="aliasController"
</button> [allowedEntityTypes]="[entityTypes.DEVICE]"
</div> [callbacks]="widgetConfigCallbacks"
</mat-expansion-panel> formControlName="targetDeviceAliasId">
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc && </tb-entity-alias-select>
modelValue?.isDataEnabled" [expanded]="true"> </div>
<mat-expansion-panel-header> </mat-expansion-panel>
<mat-panel-title> <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.alarm &&
{{ 'widget-config.target-device' | translate }} modelValue?.isDataEnabled" [expanded]="true">
</mat-panel-title> <mat-expansion-panel-header>
</mat-expansion-panel-header> <mat-panel-title>
<div [formGroup]="targetDeviceSettings" style="padding: 0 5px;"> {{ 'widget-config.alarm-source' | translate }}
<tb-entity-alias-select fxFlex </mat-panel-title>
[tbRequired]="!widgetEditMode" </mat-expansion-panel-header>
[aliasController]="aliasController" <div [formGroup]="alarmSourceSettings" style="padding: 0 5px;">
[allowedEntityTypes]="[entityTypes.DEVICE]" <section fxFlex
[callbacks]="widgetConfigCallbacks" fxLayout="column"
formControlName="targetDeviceAliasId"> fxLayoutAlign="center"
</tb-entity-alias-select> fxLayout.gt-sm="row"
</div> fxLayoutAlign.gt-sm="start center">
</mat-expansion-panel> <mat-form-field class="tb-datasource-type">
<mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.alarm && <mat-select formControlName="type">
modelValue?.isDataEnabled" [expanded]="true"> <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
<mat-expansion-panel-header> {{ datasourceTypesTranslations.get(datasourceType) | translate }}
<mat-panel-title> </mat-option>
{{ 'widget-config.alarm-source' | translate }} </mat-select>
</mat-panel-title> </mat-form-field>
</mat-expansion-panel-header> <section class="tb-datasource" [ngSwitch]="alarmSourceSettings.get('type').value">
<div [formGroup]="alarmSourceSettings" style="padding: 0 5px;"> <ng-template [ngSwitchCase]="datasourceType.entity">
<section fxFlex <tb-entity-alias-select
fxLayout="column" [showLabel]="true"
fxLayoutAlign="center" [tbRequired]="alarmSourceSettings.get('type').value === datasourceType.entity"
fxLayout.gt-sm="row" [aliasController]="aliasController"
fxLayoutAlign.gt-sm="start center"> formControlName="entityAliasId"
<mat-form-field class="tb-datasource-type"> [callbacks]="widgetConfigCallbacks">
<mat-select formControlName="type"> </tb-entity-alias-select>
<mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType"> <tb-filter-select
{{ datasourceTypesTranslations.get(datasourceType) | translate }} [showLabel]="true"
</mat-option> [aliasController]="aliasController"
</mat-select> formControlName="filterId"
</mat-form-field> [callbacks]="widgetConfigCallbacks">
<section class="tb-datasource" [ngSwitch]="alarmSourceSettings.get('type').value"> </tb-filter-select>
<ng-template [ngSwitchCase]="datasourceType.entity"> </ng-template>
<tb-entity-alias-select </section>
[showLabel]="true" <tb-data-keys class="tb-data-keys" fxFlex
[tbRequired]="alarmSourceSettings.get('type').value === datasourceType.entity" [widgetType]="widgetType"
[aliasController]="aliasController" [datasourceType]="alarmSourceSettings.get('type').value"
formControlName="entityAliasId" [aliasController]="aliasController"
[callbacks]="widgetConfigCallbacks"> [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
</tb-entity-alias-select> [callbacks]="widgetConfigCallbacks"
<tb-filter-select [entityAliasId]="alarmSourceSettings.get('entityAliasId').value"
[showLabel]="true" formControlName="dataKeys">
[aliasController]="aliasController" </tb-data-keys>
formControlName="filterId"
[callbacks]="widgetConfigCallbacks">
</tb-filter-select>
</ng-template>
</section> </section>
<tb-data-keys class="tb-data-keys" fxFlex </div>
[widgetType]="widgetType" </mat-expansion-panel>
[datasourceType]="alarmSourceSettings.get('type').value" <mat-expansion-panel [formGroup]="widgetSettings">
[aliasController]="aliasController" <mat-expansion-panel-header>
[datakeySettingsSchema]="modelValue?.dataKeySettingsSchema" <mat-panel-title translate>widget-config.data-settings</mat-panel-title>
[callbacks]="widgetConfigCallbacks" </mat-expansion-panel-header>
[entityAliasId]="alarmSourceSettings.get('entityAliasId').value" <ng-template matExpansionPanelContent>
formControlName="dataKeys"> <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
</tb-data-keys> fxLayoutGap="8px">
</section> <mat-form-field fxFlex>
</div> <mat-label translate>widget-config.units</mat-label>
</mat-expansion-panel> <input matInput formControlName="units">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.decimals</mat-label>
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</mat-accordion>
</div> </div>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'widget-config.settings' | translate }}"> <mat-tab label="{{ 'widget-config.settings' | translate }}">
<div class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px"> <div class="mat-content mat-padding" fxLayout="column">
<div [formGroup]="widgetSettings" fxLayout="column" fxLayoutGap="8px"> <div [formGroup]="widgetSettings" fxLayout="column">
<span translate>widget-config.general-settings</span> <fieldset class="fields-group" fxLayout="column">
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"> <legend class="group-title" translate>widget-config.title</legend>
<div fxLayout="column" fxLayoutAlign="center" fxFlex.sm="40%" fxFlex.gt-sm="30%"> <mat-slide-toggle formControlName="showTitle" style="margin: 8px 0">
<mat-form-field fxFlex class="mat-block"> {{ 'widget-config.display-title' | translate }}
</mat-slide-toggle>
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px">
<mat-form-field fxFlex>
<mat-label translate>widget-config.title</mat-label> <mat-label translate>widget-config.title</mat-label>
<input matInput formControlName="title"> <input matInput formControlName="title">
</mat-form-field> </mat-form-field>
@ -306,130 +330,143 @@
<input matInput formControlName="titleTooltip"> <input matInput formControlName="titleTooltip">
</mat-form-field> </mat-form-field>
</div> </div>
<div fxFlex [fxShow]="widgetSettings.get('showTitle').value"> <fieldset class="fields-group" fxLayout="column" fxLayoutGap="8px" style="margin: 0">
<tb-json-object-edit <legend class="group-title" translate>widget-config.title-icon</legend>
[editorStyle]="{minHeight: '100px'}" <mat-slide-toggle formControlName="showTitleIcon">
required
label="{{ 'widget-config.title-style' | translate }}"
formControlName="titleStyle"
></tb-json-object-edit>
</div>
</div>
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-md="row" fxLayoutAlign.gt-md="start center" fxFlex="100%"
fxLayoutGap="8px">
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px" fxFlex.gt-md>
<mat-checkbox fxFlex formControlName="showTitleIcon">
{{ 'widget-config.display-icon' | translate }} {{ 'widget-config.display-icon' | translate }}
</mat-checkbox> </mat-slide-toggle>
<tb-material-icon-select fxFlex <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row wrap" fxLayoutAlign="start center"
formControlName="titleIcon"> fxLayoutGap="8px">
</tb-material-icon-select> <tb-material-icon-select fxFlex
</div> formControlName="titleIcon">
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center" </tb-material-icon-select>
fxLayoutGap="8px" fxFlex.gt-md> <tb-color-input fxFlex
<tb-color-input fxFlex label="{{'widget-config.icon-color' | translate}}"
label="{{'widget-config.icon-color' | translate}}" icon="format_color_fill"
icon="format_color_fill" openOnInput
openOnInput formControlName="iconColor">
formControlName="iconColor"> </tb-color-input>
</tb-color-input> <mat-form-field fxFlex>
<mat-form-field fxFlex> <mat-label translate>widget-config.icon-size</mat-label>
<mat-label translate>widget-config.icon-size</mat-label> <input matInput formControlName="iconSize">
<input matInput formControlName="iconSize"> </mat-form-field>
</mat-form-field> </div>
</div> </fieldset>
</div> <mat-expansion-panel class="tb-settings">
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center" <mat-expansion-panel-header>
fxLayoutGap="8px"> <mat-panel-description fxLayoutAlign="end" translate>
<div fxLayout="column" fxLayoutAlign="center" fxLayoutGap="8px" fxFlex.sm="40%" fxFlex.gt-sm="30%"> widget-config.advanced-settings
<mat-checkbox formControlName="showTitle"> </mat-panel-description>
{{ 'widget-config.display-title' | translate }} </mat-expansion-panel-header>
</mat-checkbox> <ng-template matExpansionPanelContent>
<mat-checkbox formControlName="dropShadow"> <tb-json-object-edit
{{ 'widget-config.drop-shadow' | translate }} [editorStyle]="{minHeight: '100px'}"
</mat-checkbox> required
<mat-checkbox formControlName="enableFullscreen"> label="{{ 'widget-config.title-style' | translate }}"
{{ 'widget-config.enable-fullscreen' | translate }} formControlName="titleStyle"
</mat-checkbox> ></tb-json-object-edit>
</div> </ng-template>
<div fxFlex> </mat-expansion-panel>
<tb-json-object-edit </fieldset>
[editorStyle]="{minHeight: '100px'}" <fieldset class="fields-group" fxLayout="column">
required <legend class="group-title" translate>widget-config.widget-style</legend>
label="{{ 'widget-config.widget-style' | translate }}" <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-md="row" fxLayoutAlign.gt-md="start center"
formControlName="widgetStyle" fxFlex="100%" fxLayoutGap="8px" class="tb-widget-style">
></tb-json-object-edit> <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
</div> fxLayoutGap="8px" fxFlex.gt-md>
</div> <tb-color-input fxFlex
<div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-md="row" fxLayoutAlign.gt-md="start center" label="{{'widget-config.background-color' | translate}}"
fxFlex="100%" fxLayoutGap="8px"> icon="format_color_fill"
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center" openOnInput
fxLayoutGap="8px" fxFlex.gt-md> formControlName="backgroundColor">
<tb-color-input fxFlex </tb-color-input>
label="{{'widget-config.background-color' | translate}}" <tb-color-input fxFlex
icon="format_color_fill" label="{{'widget-config.text-color' | translate}}"
openOnInput icon="format_color_fill"
formControlName="backgroundColor"> openOnInput
</tb-color-input> formControlName="color">
<tb-color-input fxFlex </tb-color-input>
label="{{'widget-config.text-color' | translate}}" </div>
icon="format_color_fill" <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
openOnInput fxLayoutGap="8px" fxFlex.gt-md>
formControlName="color"> <mat-form-field fxFlex>
</tb-color-input> <mat-label translate>widget-config.padding</mat-label>
</div> <input matInput formControlName="padding">
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center" </mat-form-field>
fxLayoutGap="8px" fxFlex.gt-md> <mat-form-field fxFlex>
<mat-form-field fxFlex> <mat-label translate>widget-config.margin</mat-label>
<mat-label translate>widget-config.padding</mat-label> <input matInput formControlName="margin">
<input matInput formControlName="padding"> </mat-form-field>
</mat-form-field> </div>
<mat-form-field fxFlex>
<mat-label translate>widget-config.margin</mat-label>
<input matInput formControlName="margin">
</mat-form-field>
</div> </div>
</div> <mat-slide-toggle formControlName="dropShadow" style="margin-bottom: 8px">
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center" {{ 'widget-config.drop-shadow' | translate }}
fxLayoutGap="8px"> </mat-slide-toggle>
<mat-form-field fxFlex> <mat-slide-toggle formControlName="enableFullscreen">
<mat-label translate>widget-config.units</mat-label> {{ 'widget-config.enable-fullscreen' | translate }}
<input matInput formControlName="units"> </mat-slide-toggle>
</mat-form-field> <mat-expansion-panel class="tb-settings">
<mat-form-field fxFlex> <mat-expansion-panel-header>
<mat-label translate>widget-config.decimals</mat-label> <mat-panel-description fxLayoutAlign="end" translate>
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1"> widget-config.advanced-settings
</mat-form-field> </mat-panel-description>
</div> </mat-expansion-panel-header>
<div [fxShow]="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.latest" <ng-template matExpansionPanelContent>
fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center" <tb-json-object-edit
fxLayoutGap="8px" fxFlex="100%"> [editorStyle]="{minHeight: '100px'}"
<mat-checkbox fxFlex.gt-xs formControlName="showLegend"> required
{{ 'widget-config.display-legend' | translate }} label="{{ 'widget-config.widget-style' | translate }}"
</mat-checkbox> formControlName="widgetStyle"
<section fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;"> ></tb-json-object-edit>
<tb-legend-config formControlName="legendConfig"> </ng-template>
</tb-legend-config> </mat-expansion-panel>
</section> </fieldset>
</div> <fieldset class="fields-group fields-group-slider" fxLayout="column">
</div> <legend class="group-title" translate>widget-config.legend</legend>
<div [formGroup]="layoutSettings" fxLayout="column" fxLayoutGap="8px"> <mat-expansion-panel class="tb-settings">
<span translate>widget-config.mobile-mode-settings</span> <mat-expansion-panel-header fxLayout="row wrap">
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center" <mat-panel-title>
fxLayoutGap="8px"> <mat-slide-toggle formControlName="showLegend" (click)="$event.stopPropagation()" fxLayoutAlign="center">
<mat-checkbox formControlName="mobileHide"> {{ 'widget-config.display-legend' | translate }}
{{ 'widget-config.mobile-hide' | translate }} </mat-slide-toggle>
</mat-checkbox> </mat-panel-title>
<mat-form-field fxFlex> <mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
<mat-label translate>widget-config.order</mat-label> widget-config.advanced-settings
<input matInput formControlName="mobileOrder" type="number" step="1"> </mat-panel-description>
</mat-form-field> </mat-expansion-panel-header>
<mat-form-field fxFlex> <ng-template matExpansionPanelContent>
<mat-label translate>widget-config.height</mat-label> <tb-legend-config formControlName="legendConfig"></tb-legend-config>
<input matInput formControlName="mobileHeight" type="number" min="1" max="10" step="1"> </ng-template>
</mat-form-field> </mat-expansion-panel>
</div> </fieldset>
<fieldset [formGroup]="layoutSettings" class="fields-group fields-group-slider" fxLayout="column">
<legend class="group-title" translate>widget-config.mobile-mode-settings</legend>
<mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-slide-toggle formControlName="mobileHide" (click)="$event.stopPropagation()" fxLayoutAlign="center">
{{ 'widget-config.mobile-hide' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
fxLayoutGap="8px">
<mat-form-field fxFlex>
<mat-label translate>widget-config.order</mat-label>
<input matInput formControlName="mobileOrder" type="number" step="1">
</mat-form-field>
<mat-form-field fxFlex>
<mat-label translate>widget-config.height</mat-label>
<input matInput formControlName="mobileHeight" type="number" min="1" max="10" step="1">
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
</fieldset>
</div> </div>
</div> </div>
</mat-tab> </mat-tab>

58
ui-ngx/src/app/modules/home/components/widget/widget-config.component.scss

@ -17,9 +17,6 @@
:host { :host {
.tb-widget-config { .tb-widget-config {
.tb-advanced-widget-config {
height: 100%;
}
.tb-advanced-widget-config { .tb-advanced-widget-config {
height: 100%; height: 100%;
} }
@ -69,6 +66,28 @@
padding-left: 8px; padding-left: 8px;
} }
} }
.fields-group {
padding: 0 16px 8px;
margin-bottom: 10px;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
}
}
.fields-group-slider {
padding: 0;
legend {
margin-left: 16px;
}
.tb-settings {
padding: 0 16px 8px;
}
}
.tb-widget-style {
margin-top: 16px;
}
} }
} }
@ -94,6 +113,36 @@
white-space: normal; white-space: normal;
} }
.mat-expansion-panel { .mat-expansion-panel {
&.tb-settings {
box-shadow: none;
.mat-content {
overflow: visible;
}
.mat-expansion-panel-header {
padding: 0;
&:hover {
background: none;
}
.mat-expansion-indicator {
padding: 2px;
}
}
.mat-expansion-panel-header-description {
align-items: center;
}
.mat-expansion-panel-body{
padding: 0;
}
.tb-json-object-panel {
margin: 0 0 8px;
}
.mat-checkbox-layout {
margin: 5px 0;
}
.mat-checkbox-inner-container {
margin-right: 12px;
}
}
&.tb-datasources { &.tb-datasources {
&.mat-expanded { &.mat-expanded {
overflow: visible; overflow: visible;
@ -152,5 +201,8 @@
} }
} }
} }
.mat-slide-toggle-content {
white-space: normal;
}
} }
} }

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

@ -212,11 +212,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
showLegend: [null, []], showLegend: [null, []],
legendConfig: [null, []] legendConfig: [null, []]
}); });
this.widgetSettings.get('showTitle').valueChanges.subscribe((value: boolean) => {
if (value) {
this.widgetSettings.get('titleStyle').enable({emitEvent: false});
this.widgetSettings.get('titleTooltip').enable({emitEvent: false});
this.widgetSettings.get('showTitleIcon').enable({emitEvent: false});
} else {
this.widgetSettings.get('titleStyle').disable({emitEvent: false});
this.widgetSettings.get('titleTooltip').disable({emitEvent: false});
this.widgetSettings.get('showTitleIcon').patchValue(false);
this.widgetSettings.get('showTitleIcon').disable({emitEvent: false});
}
});
this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => { this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => {
if (value) { if (value) {
this.widgetSettings.get('titleIcon').enable({emitEvent: false}); this.widgetSettings.get('titleIcon').enable({emitEvent: false});
this.widgetSettings.get('iconColor').enable({emitEvent: false});
this.widgetSettings.get('iconSize').enable({emitEvent: false});
} else { } else {
this.widgetSettings.get('titleIcon').disable({emitEvent: false}); this.widgetSettings.get('titleIcon').disable({emitEvent: false});
this.widgetSettings.get('iconColor').disable({emitEvent: false});
this.widgetSettings.get('iconSize').disable({emitEvent: false});
} }
}); });
this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => { this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => {
@ -236,6 +253,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}); });
} }
ngOnDestroy(): void {
this.removeChangeSubscriptions();
}
private removeChangeSubscriptions() { private removeChangeSubscriptions() {
if (this.dataSettingsChangesSubscription) { if (this.dataSettingsChangesSubscription) {
this.dataSettingsChangesSubscription.unsubscribe(); this.dataSettingsChangesSubscription.unsubscribe();
@ -376,7 +397,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)',
iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', iconSize: isDefined(config.iconSize) ? config.iconSize : '24px',
titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '', titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '',
showTitle: config.showTitle, showTitle: isDefined(config.showTitle) ? config.showTitle : false,
dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true,
enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true,
backgroundColor: config.backgroundColor, backgroundColor: config.backgroundColor,
@ -396,11 +417,25 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
}, },
{emitEvent: false} {emitEvent: false}
); );
const showTitle: boolean = this.widgetSettings.get('showTitle').value;
if (showTitle) {
this.widgetSettings.get('titleTooltip').enable({emitEvent: false});
this.widgetSettings.get('titleStyle').enable({emitEvent: false});
this.widgetSettings.get('showTitleIcon').enable({emitEvent: false});
} else {
this.widgetSettings.get('titleTooltip').disable({emitEvent: false});
this.widgetSettings.get('titleStyle').disable({emitEvent: false});
this.widgetSettings.get('showTitleIcon').disable({emitEvent: false});
}
const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value;
if (showTitleIcon) { if (showTitleIcon) {
this.widgetSettings.get('titleIcon').enable({emitEvent: false}); this.widgetSettings.get('titleIcon').enable({emitEvent: false});
this.widgetSettings.get('iconColor').enable({emitEvent: false});
this.widgetSettings.get('iconSize').enable({emitEvent: false});
} else { } else {
this.widgetSettings.get('titleIcon').disable({emitEvent: false}); this.widgetSettings.get('titleIcon').disable({emitEvent: false});
this.widgetSettings.get('iconColor').disable({emitEvent: false});
this.widgetSettings.get('iconSize').disable({emitEvent: false});
} }
const showLegend: boolean = this.widgetSettings.get('showLegend').value; const showLegend: boolean = this.widgetSettings.get('showLegend').value;
if (showLegend) { if (showLegend) {

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

@ -279,6 +279,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
this.widgetContext.servicesMap = ServicesMap; this.widgetContext.servicesMap = ServicesMap;
this.widgetContext.isEdit = this.isEdit; this.widgetContext.isEdit = this.isEdit;
this.widgetContext.isMobile = this.isMobile; this.widgetContext.isMobile = this.isMobile;
this.widgetContext.toastTargetId = this.toastTargetId;
this.widgetContext.subscriptionApi = { this.widgetContext.subscriptionApi = {
createSubscription: this.createSubscription.bind(this), createSubscription: this.createSubscription.bind(this),

1
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -230,6 +230,7 @@ export class WidgetContext {
$scope: IDynamicWidgetComponent; $scope: IDynamicWidgetComponent;
isEdit: boolean; isEdit: boolean;
isMobile: boolean; isMobile: boolean;
toastTargetId: string;
widgetNamespace?: string; widgetNamespace?: string;
subscriptionApi?: WidgetSubscriptionApi; subscriptionApi?: WidgetSubscriptionApi;

9
ui-ngx/src/app/shared/components/marked-options.service.ts

@ -21,6 +21,7 @@ import { DOCUMENT } from '@angular/common';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { Tokenizer } from 'marked'; import { Tokenizer } from 'marked';
import * as marked from 'marked'; import * as marked from 'marked';
import { Clipboard } from '@angular/cdk/clipboard';
const copyCodeBlock = '{:copy-code}'; const copyCodeBlock = '{:copy-code}';
const codeStyleRegex = '^{:code-style="(.*)"}\n'; const codeStyleRegex = '^{:code-style="(.*)"}\n';
@ -47,6 +48,7 @@ export class MarkedOptionsService extends MarkedOptions {
private id = 1; private id = 1;
constructor(private translate: TranslateService, constructor(private translate: TranslateService,
private clipboardService: Clipboard,
@Inject(WINDOW) private readonly window: Window, @Inject(WINDOW) private readonly window: Window,
@Inject(DOCUMENT) private readonly document: Document) { @Inject(DOCUMENT) private readonly document: Document) {
super(); super();
@ -162,7 +164,7 @@ export class MarkedOptionsService extends MarkedOptions {
const copyWrapper = $('#codeWrapper' + id); const copyWrapper = $('#codeWrapper' + id);
if (copyWrapper.hasClass('noChars')) { if (copyWrapper.hasClass('noChars')) {
const text = decodeURIComponent($('#copyCodeId' + id).text()); const text = decodeURIComponent($('#copyCodeId' + id).text());
this.window.navigator.clipboard.writeText(text).then(() => { if (this.clipboardService.copy(text)) {
import('tooltipster').then( import('tooltipster').then(
() => { () => {
if (!copyWrapper.hasClass('tooltipstered')) { if (!copyWrapper.hasClass('tooltipstered')) {
@ -186,9 +188,8 @@ export class MarkedOptionsService extends MarkedOptions {
} }
const tooltip = copyWrapper.tooltipster('instance'); const tooltip = copyWrapper.tooltipster('instance');
tooltip.open(); tooltip.open();
} });
); }
});
} }
} }
} }

18
ui-ngx/src/app/shared/components/material-icon-select.component.ts

@ -92,14 +92,16 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
} }
openIconDialog() { openIconDialog() {
this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe( if (!this.disabled) {
(icon) => { this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe(
if (icon) { (icon) => {
this.materialIconFormGroup.patchValue( if (icon) {
{icon}, {emitEvent: true} this.materialIconFormGroup.patchValue(
); {icon}, {emitEvent: true}
);
}
} }
} );
); }
} }
} }

43
ui-ngx/src/app/shared/components/protobuf-content.component.html

@ -0,0 +1,43 @@
<!--
Copyright © 2016-2021 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.
-->
<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
tb-fullscreen
[fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-protobuf-content-toolbar">
<label class="tb-title no-padding">{{ label }}</label>
<span fxFlex></span>
<button type="button"
mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="beautifyProtobuf()">
{{'js-func.tidy' | translate }}
</button>
<fieldset style="width: initial">
<div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
matTooltipPosition="above"
style="border-radius: 50%"
(click)="fullscreen = !fullscreen">
<button type='button' mat-button mat-icon-button class="tb-mat-32">
<mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
</button>
</div>
</fieldset>
</div>
<div id="tb-protobuf-panel" tb-toast toastTarget="{{toastTargetId}}"
class="tb-protobuf-content-panel" fxLayout="column">
<div #protobufEditor id="tb-protobuf-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div>
</div>
</div>

57
ui-ngx/src/app/shared/components/protobuf-content.component.scss

@ -0,0 +1,57 @@
/**
* Copyright © 2016-2021 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.
*/
:host {
position: relative;
.fill-height {
height: 100%;
}
}
.tb-protobuf-content-toolbar {
button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
align-items: center;
vertical-align: middle;
min-width: 32px;
min-height: 15px;
padding: 4px;
margin: 0;
font-size: .8rem;
line-height: 15px;
color: #7b7b7b;
background: rgba(220, 220, 220, .35);
&:not(:last-child) {
margin-right: 4px;
}
}
}
.tb-protobuf-content-panel {
height: 100%;
margin-left: 15px;
border: 1px solid #c0c0c0;
#tb-protobuf-input {
width: 100%;
min-width: 200px;
min-height: 160px;
height: 100%;
&:not(.fill-height) {
min-height: 200px;
}
}
}

193
ui-ngx/src/app/shared/components/protobuf-content.component.ts

@ -0,0 +1,193 @@
///
/// Copyright © 2016-2021 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.
///
import {
Component,
ElementRef,
forwardRef,
Input,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Ace } from 'ace-builds';
import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
import { ResizeObserver } from '@juggle/resize-observer';
import { guid } from '@core/utils';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { getAce } from '@shared/models/ace/ace.models';
import { beautifyJs } from '@shared/models/beautify.models';
@Component({
selector: 'tb-protobuf-content',
templateUrl: './protobuf-content.component.html',
styleUrls: ['./protobuf-content.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ProtobufContentComponent),
multi: true
}
]
})
export class ProtobufContentComponent implements OnInit, ControlValueAccessor, OnDestroy {
@ViewChild('protobufEditor', {static: true})
protobufEditorElmRef: ElementRef;
private protobufEditor: Ace.Editor;
private editorsResizeCaf: CancelAnimationFrame;
private editorResize$: ResizeObserver;
private ignoreChange = false;
toastTargetId = `protobufContentEditor-${guid()}`;
@Input() label: string;
@Input() disabled: boolean;
@Input() fillHeight: boolean;
@Input() editorStyle: {[klass: string]: any};
@Input() tbPlaceholder: string;
private readonlyValue: boolean;
get readonly(): boolean {
return this.readonlyValue;
}
@Input()
set readonly(value: boolean) {
this.readonlyValue = coerceBooleanProperty(value);
}
fullscreen = false;
contentBody: string;
errorShowed = false;
private propagateChange = null;
constructor(public elementRef: ElementRef,
protected store: Store<AppState>,
private raf: RafService) {
}
ngOnInit(): void {
const editorElement = this.protobufEditorElmRef.nativeElement;
let editorOptions: Partial<Ace.EditorOptions> = {
mode: `ace/mode/protobuf`,
showGutter: true,
showPrintMargin: false,
readOnly: this.disabled || this.readonly,
};
const advancedOptions = {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
};
editorOptions = {...editorOptions, ...advancedOptions};
getAce().subscribe(
(ace) => {
this.protobufEditor = ace.edit(editorElement, editorOptions);
this.protobufEditor.session.setUseWrapMode(true);
this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
this.protobufEditor.setReadOnly(this.disabled || this.readonly);
this.protobufEditor.on('change', () => {
if (!this.ignoreChange) {
this.updateView();
}
});
this.editorResize$ = new ResizeObserver(() => {
this.onAceEditorResize();
});
this.editorResize$.observe(editorElement);
}
);
}
ngOnDestroy(): void {
if (this.editorResize$) {
this.editorResize$.disconnect();
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.protobufEditor) {
this.protobufEditor.setReadOnly(this.disabled || this.readonly);
}
}
writeValue(value: string): void {
this.contentBody = value;
if (this.protobufEditor) {
this.ignoreChange = true;
this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
this.ignoreChange = false;
}
}
updateView() {
const editorValue = this.protobufEditor.getValue();
if (this.contentBody !== editorValue) {
this.contentBody = editorValue;
this.propagateChange(this.contentBody);
}
}
beautifyProtobuf() {
beautifyJs(this.contentBody, {indent_size: 4, wrap_line_length: 60}).subscribe(
(res) => {
this.protobufEditor.setValue(res ? res : '', -1);
this.updateView();
}
);
}
onFullscreen() {
if (this.protobufEditor) {
setTimeout(() => {
this.protobufEditor.resize();
}, 0);
}
}
private onAceEditorResize() {
if (this.editorsResizeCaf) {
this.editorsResizeCaf();
this.editorsResizeCaf = null;
}
this.editorsResizeCaf = this.raf.raf(() => {
this.protobufEditor.resize();
this.protobufEditor.renderer.updateFull();
});
}
}

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

@ -36,6 +36,8 @@ export function loadAceDependencies(): Observable<any> {
aceObservables.push(from(import('ace-builds/src-noconflict/mode-text'))); aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown'))); aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-html'))); aceObservables.push(from(import('ace-builds/src-noconflict/mode-html')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-c_cpp')));
aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json')));
@ -43,6 +45,8 @@ export function loadAceDependencies(): Observable<any> {
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html'))); aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/c_cpp')));
aceObservables.push(from(import('ace-builds/src-noconflict/snippets/protobuf')));
aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate'))); aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate')));
aceObservables.push(from(import('ace-builds/src-noconflict/theme-github'))); aceObservables.push(from(import('ace-builds/src-noconflict/theme-github')));
return forkJoin(aceObservables).pipe( return forkJoin(aceObservables).pipe(

7
ui-ngx/src/app/shared/shared.module.ts

@ -155,6 +155,7 @@ import { MarkedOptionsService } from '@shared/components/marked-options.service'
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens'; import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
import { TbMarkdownComponent } from '@shared/components/markdown.component'; import { TbMarkdownComponent } from '@shared/components/markdown.component';
import { ProtobufContentComponent } from './components/protobuf-content.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService; return markedOptionsService;
@ -268,7 +269,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
OtaPackageAutocompleteComponent, OtaPackageAutocompleteComponent,
WidgetsBundleSearchComponent, WidgetsBundleSearchComponent,
CopyButtonComponent, CopyButtonComponent,
TogglePasswordComponent TogglePasswordComponent,
ProtobufContentComponent
], ],
imports: [ imports: [
CommonModule, CommonModule,
@ -458,7 +460,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
OtaPackageAutocompleteComponent, OtaPackageAutocompleteComponent,
WidgetsBundleSearchComponent, WidgetsBundleSearchComponent,
CopyButtonComponent, CopyButtonComponent,
TogglePasswordComponent TogglePasswordComponent,
ProtobufContentComponent
] ]
}) })
export class SharedModule { } export class SharedModule { }

6
ui-ngx/src/assets/locale/locale.constant-el_GR.json

@ -596,8 +596,8 @@
"make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε το dashboard '{{dashboardTitle}}' ιδιωτικό", "make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε το dashboard '{{dashboardTitle}}' ιδιωτικό",
"make-private-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα γίνουν ιδιωτικά και δεν θα είναι διαθέσιμα από τρίτους.", "make-private-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα γίνουν ιδιωτικά και δεν θα είναι διαθέσιμα από τρίτους.",
"make-private-dashboard": "Κάνε το dashboard ιδιωτικό", "make-private-dashboard": "Κάνε το dashboard ιδιωτικό",
"socialshare-text": "'{{dashboardTitle}}' powered by EyeTech", "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
"socialshare-title": "'{{dashboardTitle}}' powered by EyeTech", "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
"select-dashboard": "Επιλογή dashboard", "select-dashboard": "Επιλογή dashboard",
"no-dashboards-matching": "Δεν βρέθηκαν dashboards που να αντιστοιχούν σε '{{entity}}'.", "no-dashboards-matching": "Δεν βρέθηκαν dashboards που να αντιστοιχούν σε '{{entity}}'.",
"dashboard-required": "Απαιτείται dashboard.", "dashboard-required": "Απαιτείται dashboard.",
@ -2496,7 +2496,7 @@
"domain-name": "Όνομα Domain", "domain-name": "Όνομα Domain",
"help-link-base-url": "Base url για συνδέσμους βοηθείας", "help-link-base-url": "Base url για συνδέσμους βοηθείας",
"enable-help-links": "Ενεργοποίηση συνδέσμων βοηθείας", "enable-help-links": "Ενεργοποίηση συνδέσμων βοηθείας",
"error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: gprs.cloud", "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: thingsboard.io",
"show-platform-name-version": "Εμφάνιση ονόματος και έκδοσης πλατφόρμας", "show-platform-name-version": "Εμφάνιση ονόματος και έκδοσης πλατφόρμας",
"platform-name": "Όνομα πλατφόρμας", "platform-name": "Όνομα πλατφόρμας",
"platform-version": "Έκδοση πλατφόρμας", "platform-version": "Έκδοση πλατφόρμας",

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

@ -3030,7 +3030,7 @@
"title": "Title", "title": "Title",
"title-tooltip": "Title Tooltip", "title-tooltip": "Title Tooltip",
"general-settings": "General settings", "general-settings": "General settings",
"display-title": "Display title", "display-title": "Display widget title",
"drop-shadow": "Drop shadow", "drop-shadow": "Drop shadow",
"enable-fullscreen": "Enable fullscreen", "enable-fullscreen": "Enable fullscreen",
"background-color": "Background color", "background-color": "Background color",
@ -3039,7 +3039,7 @@
"margin": "Margin", "margin": "Margin",
"widget-style": "Widget style", "widget-style": "Widget style",
"title-style": "Title style", "title-style": "Title style",
"mobile-mode-settings": "Mobile mode settings", "mobile-mode-settings": "Mobile mode",
"order": "Order", "order": "Order",
"height": "Height", "height": "Height",
"mobile-hide": "Hide widget in mobile mode", "mobile-hide": "Hide widget in mobile mode",
@ -3048,6 +3048,7 @@
"timewindow": "Timewindow", "timewindow": "Timewindow",
"use-dashboard-timewindow": "Use dashboard timewindow", "use-dashboard-timewindow": "Use dashboard timewindow",
"display-timewindow": "Display timewindow", "display-timewindow": "Display timewindow",
"legend": "Legend",
"display-legend": "Display legend", "display-legend": "Display legend",
"datasources": "Datasources", "datasources": "Datasources",
"maximum-datasources": "Maximum { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", "maximum-datasources": "Maximum { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }",
@ -3075,9 +3076,12 @@
"delete-action": "Delete action", "delete-action": "Delete action",
"delete-action-title": "Delete widget action", "delete-action-title": "Delete widget action",
"delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?", "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?",
"title-icon": "Title icon",
"display-icon": "Display title icon", "display-icon": "Display title icon",
"icon-color": "Icon color", "icon-color": "Icon color",
"icon-size": "Icon size" "icon-size": "Icon size",
"advanced-settings": "Advanced settings",
"data-settings": "Data settings"
}, },
"widget-type": { "widget-type": {
"import": "Import widget type", "import": "Import widget type",

4
ui-ngx/src/assets/locale/locale.constant-fr_FR.json

@ -1470,7 +1470,7 @@
"rulechain-required": "Chaîne de règles requise", "rulechain-required": "Chaîne de règles requise",
"rulechains": "Chaînes de règles", "rulechains": "Chaînes de règles",
"select-rulechain": "Sélectionner la chaîne de règles", "select-rulechain": "Sélectionner la chaîne de règles",
"set-root": "Rend la chaîne de règles racine (root) ", "set-root": "Rendre la chaîne de règles racine (root) ",
"set-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra racine (root) et gérera tous les messages de transport entrants.", "set-root-rulechain-text": "Après la confirmation, la chaîne de règles deviendra racine (root) et gérera tous les messages de transport entrants.",
"set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?", "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?",
"system": "Système", "system": "Système",
@ -1498,7 +1498,7 @@
"edge-template-root": "Racine du modèle", "edge-template-root": "Racine du modèle",
"search": "Rechercher des chaînes de règles", "search": "Rechercher des chaînes de règles",
"selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains} } sélectionné", "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains} } sélectionné",
"open-rulechain": "Chaîne de règles ouverte", "open-rulechain": "Ouvrir la Chaîne de règles",
"assign-to-edge": "Attribuer à Bordure", "assign-to-edge": "Attribuer à Bordure",
"edge-rulechain": "Chaîne de règles Bordure", "edge-rulechain": "Chaîne de règles Bordure",
"unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains} }?" "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains} }?"

2
ui-ngx/src/assets/locale/locale.constant-ka_GE.json

@ -492,7 +492,7 @@
"make-private-dashboard-text": "დადასტურების შემდეგ დეშბორდი გახდება პრივატული და აღარ იქნება ხელმისაწვდომი სხვა მომხმარებლებისთვის", "make-private-dashboard-text": "დადასტურების შემდეგ დეშბორდი გახდება პრივატული და აღარ იქნება ხელმისაწვდომი სხვა მომხმარებლებისთვის",
"make-private-dashboard": "აქციე დეშბორდი პრივატულად", "make-private-dashboard": "აქციე დეშბორდი პრივატულად",
"socialshare-text": "სოციალური ტექსტი", "socialshare-text": "სოციალური ტექსტი",
"socialshare-title": "'{{dashboardTitle}}' Powered by Giot", "socialshare-title": "'{{dashboardTitle}}' Powered by ThingsBoard",
"select-dashboard": "აირჩიე დეშბორდი", "select-dashboard": "აირჩიე დეშბორდი",
"no-dashboards-matching": "'{{entity}}'-ს მზგავსი დეშბორდი არ იქნა ნაპოვნი.", "no-dashboards-matching": "'{{entity}}'-ს მზგავსი დეშბორდი არ იქნა ნაპოვნი.",
"dashboard-required": "დეშბორდი აუცილებელია", "dashboard-required": "დეშბორდი აუცილებელია",

4
ui-ngx/src/assets/locale/locale.constant-lv_LV.json

@ -453,8 +453,8 @@
"make-private-dashboard-title": "Vai esat pārliecināts, ka vēlaties veidot paneli '{{dashboardTitle}}' privātu?", "make-private-dashboard-title": "Vai esat pārliecināts, ka vēlaties veidot paneli '{{dashboardTitle}}' privātu?",
"make-private-dashboard-text": "Pēc apstiprinājuma panelis būs privāts un nebūs pieejams citiem.", "make-private-dashboard-text": "Pēc apstiprinājuma panelis būs privāts un nebūs pieejams citiem.",
"make-private-dashboard": "Veidot paneli privātu", "make-private-dashboard": "Veidot paneli privātu",
"socialshare-text": "'{{dashboardTitle}}' atbalsts no TeT", "socialshare-text": "'{{dashboardTitle}}' atbalsts no ThingsBoard",
"socialshare-title": "'{{dashboardTitle}}' atbalsts no TeT", "socialshare-title": "'{{dashboardTitle}}' atbalsts no ThingsBoard",
"select-dashboard": "Atlasīt paneli", "select-dashboard": "Atlasīt paneli",
"no-dashboards-matching": "Nav atbilstoši paneļi '{{entity}}' atrasti.", "no-dashboards-matching": "Nav atbilstoši paneļi '{{entity}}' atrasti.",
"dashboard-required": "Penelis ir nepieciešams.", "dashboard-required": "Penelis ir nepieciešams.",

12
ui-ngx/src/assets/locale/locale.constant-ru_RU.json

@ -1631,7 +1631,7 @@
"advanced": "Дополнительно", "advanced": "Дополнительно",
"title": "Название", "title": "Название",
"general-settings": "Общие настройки", "general-settings": "Общие настройки",
"display-title": "Показать название", "display-title": "Показать название на виджете",
"drop-shadow": "Тень", "drop-shadow": "Тень",
"enable-fullscreen": "Во весь экран", "enable-fullscreen": "Во весь экран",
"background-color": "Цвет фона", "background-color": "Цвет фона",
@ -1640,7 +1640,7 @@
"margin": "Margin", "margin": "Margin",
"widget-style": "Стиль виджета", "widget-style": "Стиль виджета",
"title-style": "Стиль названия", "title-style": "Стиль названия",
"mobile-mode-settings": "Настройки мобильного режима", "mobile-mode-settings": "Мобильный режим",
"order": "Порядок", "order": "Порядок",
"height": "Высота", "height": "Высота",
"units": "Специальный символ после значения", "units": "Специальный символ после значения",
@ -1648,6 +1648,7 @@
"timewindow": "Временное окно", "timewindow": "Временное окно",
"use-dashboard-timewindow": "Использовать временное окно дашборда", "use-dashboard-timewindow": "Использовать временное окно дашборда",
"display-timewindow": "Показывать временное окно", "display-timewindow": "Показывать временное окно",
"legend": "Легенда",
"display-legend": "Показать легенду", "display-legend": "Показать легенду",
"datasources": "Источники данных", "datasources": "Источники данных",
"maximum-datasources": "Максимальной количество источников данных равно {{count}}", "maximum-datasources": "Максимальной количество источников данных равно {{count}}",
@ -1673,9 +1674,12 @@
"delete-action": "Удалить действие", "delete-action": "Удалить действие",
"delete-action-title": "Удалить действие виджета", "delete-action-title": "Удалить действие виджета",
"delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?", "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?",
"display-icon": "Показывать иконку в названии", "title-icon": "Иконка в названии виджета",
"display-icon": "Показывать иконку в названии виджета",
"icon-color": "Цвет иконки", "icon-color": "Цвет иконки",
"icon-size": "Размер иконки" "icon-size": "Размер иконки",
"advanced-settings": "Расширенные настройки",
"data-settings": "Настройки данных"
}, },
"widget-type": { "widget-type": {
"import": "Импортировать тип виджета", "import": "Импортировать тип виджета",

12
ui-ngx/src/assets/locale/locale.constant-uk_UA.json

@ -2202,7 +2202,7 @@
"advanced": "Додатково", "advanced": "Додатково",
"title": "Назва", "title": "Назва",
"general-settings": "Загальні налаштування", "general-settings": "Загальні налаштування",
"display-title": "Відобразити назву", "display-title": "Відобразити назву у віджеті",
"drop-shadow": "Тінь", "drop-shadow": "Тінь",
"enable-fullscreen": "Увімкнути повноекранний режим", "enable-fullscreen": "Увімкнути повноекранний режим",
"enable-data-export": "Увімкнути експорт даних", "enable-data-export": "Увімкнути експорт даних",
@ -2212,7 +2212,7 @@
"margin": "Границі", "margin": "Границі",
"widget-style": "Стиль віджетів", "widget-style": "Стиль віджетів",
"title-style": "Стиль заголовка", "title-style": "Стиль заголовка",
"mobile-mode-settings": "Налаштування мобільного режиму", "mobile-mode-settings": "мобільний режим",
"order": "Порядок", "order": "Порядок",
"height": "Висота", "height": "Висота",
"units": "Спеціальний символ після значення", "units": "Спеціальний символ після значення",
@ -2220,6 +2220,7 @@
"timewindow": "Вікно часу", "timewindow": "Вікно часу",
"use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації", "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації",
"display-timewindow": "Показувати вікно часу", "display-timewindow": "Показувати вікно часу",
"legend": "Легенда",
"display-legend": "Показати легенду", "display-legend": "Показати легенду",
"datasources": "Джерела даних", "datasources": "Джерела даних",
"maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }", "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }",
@ -2245,9 +2246,12 @@
"delete-action": "Видалити дію", "delete-action": "Видалити дію",
"delete-action-title": "Видалити дію віджета", "delete-action-title": "Видалити дію віджета",
"delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?", "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?",
"display-icon": "Показувати іконку у назві", "title-icon": "Іконка у назві віджету",
"display-icon": "Показувати іконку у назві віджету",
"icon-color": "Колір іконки", "icon-color": "Колір іконки",
"icon-size": "Розмір іконки" "icon-size": "Розмір іконки",
"advanced-settings": "Розширені налаштування",
"data-settings": "Налаштування даних"
}, },
"widget-type": { "widget-type": {
"import": "Імпортувати тип віджета", "import": "Імпортувати тип віджета",

Loading…
Cancel
Save