Commit 4991bb725d7c4c791f13e3625e70114b1ea0d1cc

Authored by Igor Kulikov
2 parents 79588a7b c783a2a6

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

Showing 30 changed files with 1190 additions and 702 deletions
@@ -182,7 +182,8 @@ public class TbHttpClient { @@ -182,7 +182,8 @@ public class TbHttpClient {
182 HttpMethod method = HttpMethod.valueOf(config.getRequestMethod()); 182 HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
183 HttpEntity<String> entity; 183 HttpEntity<String> entity;
184 if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) || 184 if(HttpMethod.GET.equals(method) || HttpMethod.HEAD.equals(method) ||
185 - HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method)) { 185 + HttpMethod.OPTIONS.equals(method) || HttpMethod.TRACE.equals(method) ||
  186 + config.isIgnoreRequestBody()) {
186 entity = new HttpEntity<>(headers); 187 entity = new HttpEntity<>(headers);
187 } else { 188 } else {
188 entity = new HttpEntity<>(msg.getData(), headers); 189 entity = new HttpEntity<>(msg.getData(), headers);
@@ -47,6 +47,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA @@ -47,6 +47,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
47 private String proxyPassword; 47 private String proxyPassword;
48 private String proxyScheme; 48 private String proxyScheme;
49 private ClientCredentials credentials; 49 private ClientCredentials credentials;
  50 + private boolean ignoreRequestBody;
50 51
51 @Override 52 @Override
52 public TbRestApiCallNodeConfiguration defaultConfiguration() { 53 public TbRestApiCallNodeConfiguration defaultConfiguration() {
@@ -61,6 +62,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA @@ -61,6 +62,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
61 configuration.setTrimQueue(false); 62 configuration.setTrimQueue(false);
62 configuration.setEnableProxy(false); 63 configuration.setEnableProxy(false);
63 configuration.setCredentials(new AnonymousCredentials()); 64 configuration.setCredentials(new AnonymousCredentials());
  65 + configuration.setIgnoreRequestBody(false);
64 return configuration; 66 return configuration;
65 } 67 }
66 68
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.rest;
  17 +
  18 +import static org.junit.Assert.*;
  19 +import static org.mockito.Mockito.verify;
  20 +
  21 +import java.io.IOException;
  22 +import java.util.Collections;
  23 +import java.util.concurrent.CountDownLatch;
  24 +import java.util.concurrent.TimeUnit;
  25 +
  26 +import org.apache.http.Header;
  27 +import org.apache.http.HttpException;
  28 +import org.apache.http.HttpRequest;
  29 +import org.apache.http.HttpResponse;
  30 +import org.apache.http.config.SocketConfig;
  31 +import org.apache.http.impl.bootstrap.HttpServer;
  32 +import org.apache.http.impl.bootstrap.ServerBootstrap;
  33 +import org.apache.http.protocol.HttpContext;
  34 +import org.apache.http.protocol.HttpRequestHandler;
  35 +import org.junit.After;
  36 +import org.junit.Test;
  37 +import org.junit.runner.RunWith;
  38 +import org.mockito.ArgumentCaptor;
  39 +import org.mockito.Mock;
  40 +import org.mockito.junit.MockitoJUnitRunner;
  41 +import org.thingsboard.rule.engine.api.TbContext;
  42 +import org.thingsboard.rule.engine.api.TbEmail;
  43 +import org.thingsboard.rule.engine.api.TbNodeConfiguration;
  44 +import org.thingsboard.rule.engine.api.TbNodeException;
  45 +import org.thingsboard.server.common.data.id.DeviceId;
  46 +import org.thingsboard.server.common.data.id.EntityId;
  47 +import org.thingsboard.server.common.data.id.RuleChainId;
  48 +import org.thingsboard.server.common.data.id.RuleNodeId;
  49 +import org.thingsboard.server.common.msg.TbMsg;
  50 +import org.thingsboard.server.common.msg.TbMsgDataType;
  51 +import org.thingsboard.server.common.msg.TbMsgMetaData;
  52 +
  53 +import com.datastax.oss.driver.api.core.uuid.Uuids;
  54 +import com.fasterxml.jackson.databind.ObjectMapper;
  55 +
  56 +@RunWith(MockitoJUnitRunner.class)
  57 +public class TbRestApiCallNodeTest {
  58 +
  59 + private TbRestApiCallNode restNode;
  60 +
  61 + @Mock
  62 + private TbContext ctx;
  63 +
  64 + private EntityId originator = new DeviceId(Uuids.timeBased());
  65 + private TbMsgMetaData metaData = new TbMsgMetaData();
  66 +
  67 + private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased());
  68 + private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
  69 +
  70 + private HttpServer server;
  71 +
  72 + public void setupServer(String pattern, HttpRequestHandler handler) throws IOException {
  73 + SocketConfig config = SocketConfig.custom().setSoReuseAddress(true).setTcpNoDelay(true).build();
  74 + server = ServerBootstrap.bootstrap()
  75 + .setSocketConfig(config)
  76 + .registerHandler(pattern, handler)
  77 + .create();
  78 + server.start();
  79 + }
  80 +
  81 + private void initWithConfig(TbRestApiCallNodeConfiguration config) {
  82 + try {
  83 + ObjectMapper mapper = new ObjectMapper();
  84 + TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
  85 + restNode = new TbRestApiCallNode();
  86 + restNode.init(ctx, nodeConfiguration);
  87 + } catch (TbNodeException ex) {
  88 + throw new IllegalStateException(ex);
  89 + }
  90 + }
  91 +
  92 + @After
  93 + public void teardown() {
  94 + server.stop();
  95 + }
  96 +
  97 + @Test
  98 + public void deleteRequestWithoutBody() throws IOException, InterruptedException {
  99 + final CountDownLatch latch = new CountDownLatch(1);
  100 + final String path = "/path/to/delete";
  101 + setupServer("*", new HttpRequestHandler() {
  102 +
  103 + @Override
  104 + public void handle(HttpRequest request, HttpResponse response, HttpContext context)
  105 + throws HttpException, IOException {
  106 + try {
  107 + assertEquals("Request path matches", request.getRequestLine().getUri(), path);
  108 + assertFalse("Content-Type not included", request.containsHeader("Content-Type"));
  109 + assertTrue("Custom header included", request.containsHeader("Foo"));
  110 + assertEquals("Custom header value", "Bar", request.getFirstHeader("Foo").getValue());
  111 + response.setStatusCode(200);
  112 + new Thread(new Runnable() {
  113 + @Override
  114 + public void run() {
  115 + try {
  116 + Thread.sleep(1000L);
  117 + } catch (InterruptedException e) {
  118 + // ignore
  119 + } finally {
  120 + latch.countDown();
  121 + }
  122 + }
  123 + }).start();
  124 + } catch ( Exception e ) {
  125 + System.out.println("Exception handling request: " + e.toString());
  126 + e.printStackTrace();
  127 + latch.countDown();
  128 + }
  129 + }
  130 + });
  131 +
  132 + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
  133 + config.setRequestMethod("DELETE");
  134 + config.setHeaders(Collections.singletonMap("Foo", "Bar"));
  135 + config.setIgnoreRequestBody(true);
  136 + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path));
  137 + initWithConfig(config);
  138 +
  139 + TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
  140 + restNode.onMsg(ctx, msg);
  141 +
  142 + assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS));
  143 +
  144 + ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
  145 + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
  146 + ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
  147 + ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
  148 + ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
  149 + verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
  150 +
  151 +
  152 + assertEquals("USER", typeCaptor.getValue());
  153 + assertEquals(originator, originatorCaptor.getValue());
  154 + assertNotSame(metaData, metadataCaptor.getValue());
  155 + assertEquals("{}", dataCaptor.getValue());
  156 + }
  157 +
  158 + @Test
  159 + public void deleteRequestWithBody() throws IOException, InterruptedException {
  160 + final CountDownLatch latch = new CountDownLatch(1);
  161 + final String path = "/path/to/delete";
  162 + setupServer("*", new HttpRequestHandler() {
  163 +
  164 + @Override
  165 + public void handle(HttpRequest request, HttpResponse response, HttpContext context)
  166 + throws HttpException, IOException {
  167 + try {
  168 + assertEquals("Request path matches", path, request.getRequestLine().getUri());
  169 + assertTrue("Content-Type included", request.containsHeader("Content-Type"));
  170 + assertEquals("Content-Type value", "text/plain;charset=ISO-8859-1",
  171 + request.getFirstHeader("Content-Type").getValue());
  172 + assertTrue("Content-Length included", request.containsHeader("Content-Length"));
  173 + assertEquals("Content-Length value", "2",
  174 + request.getFirstHeader("Content-Length").getValue());
  175 + assertTrue("Custom header included", request.containsHeader("Foo"));
  176 + assertEquals("Custom header value", "Bar", request.getFirstHeader("Foo").getValue());
  177 + response.setStatusCode(200);
  178 + new Thread(new Runnable() {
  179 + @Override
  180 + public void run() {
  181 + try {
  182 + Thread.sleep(1000L);
  183 + } catch (InterruptedException e) {
  184 + // ignore
  185 + } finally {
  186 + latch.countDown();
  187 + }
  188 + }
  189 + }).start();
  190 + } catch ( Exception e ) {
  191 + System.out.println("Exception handling request: " + e.toString());
  192 + e.printStackTrace();
  193 + latch.countDown();
  194 + }
  195 + }
  196 + });
  197 +
  198 + TbRestApiCallNodeConfiguration config = new TbRestApiCallNodeConfiguration().defaultConfiguration();
  199 + config.setRequestMethod("DELETE");
  200 + config.setHeaders(Collections.singletonMap("Foo", "Bar"));
  201 + config.setIgnoreRequestBody(false);
  202 + config.setRestEndpointUrlPattern(String.format("http://localhost:%d%s", server.getLocalPort(), path));
  203 + initWithConfig(config);
  204 +
  205 + TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
  206 + restNode.onMsg(ctx, msg);
  207 +
  208 + assertTrue("Server handled request", latch.await(10, TimeUnit.SECONDS));
  209 +
  210 + ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
  211 + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
  212 + ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
  213 + ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
  214 + ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
  215 + verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
  216 +
  217 + assertEquals("USER", typeCaptor.getValue());
  218 + assertEquals(originator, originatorCaptor.getValue());
  219 + assertNotSame(metaData, metadataCaptor.getValue());
  220 + assertEquals("{}", dataCaptor.getValue());
  221 + }
  222 +
  223 +}
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}" tb-toast toastTarget="dashboardRoot" 18 +<div class="tb-dashboard-page mat-content" [ngClass]="{'mobile-app': isMobileApp && !isEdit}"
19 fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen"> 19 fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
20 <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet> 20 <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
21 <section class="tb-dashboard-toolbar" 21 <section class="tb-dashboard-toolbar"
@@ -139,6 +139,7 @@ @@ -139,6 +139,7 @@
139 </tb-dashboard-toolbar> 139 </tb-dashboard-toolbar>
140 </section> 140 </section>
141 <section class="tb-dashboard-container tb-absolute-fill" 141 <section class="tb-dashboard-container tb-absolute-fill"
  142 + tb-toast toastTarget="dashboardRoot"
142 #dashboardContainer 143 #dashboardContainer
143 [ngClass]="{ 'is-fullscreen': forceFullscreen, 144 [ngClass]="{ 'is-fullscreen': forceFullscreen,
144 'tb-dashboard-toolbar-opened': toolbarOpened, 145 'tb-dashboard-toolbar-opened': toolbarOpened,
@@ -49,7 +49,6 @@ import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias- @@ -49,7 +49,6 @@ import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-
49 import { DataKeysComponent } from '@home/components/widget/data-keys.component'; 49 import { DataKeysComponent } from '@home/components/widget/data-keys.component';
50 import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component'; 50 import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component';
51 import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; 51 import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
52 -import { LegendConfigPanelComponent } from '@home/components/widget/legend-config-panel.component';  
53 import { LegendConfigComponent } from '@home/components/widget/legend-config.component'; 52 import { LegendConfigComponent } from '@home/components/widget/legend-config.component';
54 import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component'; 53 import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component';
55 import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component'; 54 import { WidgetActionDialogComponent } from '@home/components/widget/action/widget-action-dialog.component';
@@ -182,7 +181,6 @@ import { DeviceProfileCommonModule } from '@home/components/profile/device/commo @@ -182,7 +181,6 @@ import { DeviceProfileCommonModule } from '@home/components/profile/device/commo
182 DataKeysComponent, 181 DataKeysComponent,
183 DataKeyConfigComponent, 182 DataKeyConfigComponent,
184 DataKeyConfigDialogComponent, 183 DataKeyConfigDialogComponent,
185 - LegendConfigPanelComponent,  
186 LegendConfigComponent, 184 LegendConfigComponent,
187 ManageWidgetActionsComponent, 185 ManageWidgetActionsComponent,
188 WidgetActionDialogComponent, 186 WidgetActionDialogComponent,
@@ -47,35 +47,50 @@ @@ -47,35 +47,50 @@
47 </mat-error> 47 </mat-error>
48 </mat-form-field> 48 </mat-form-field>
49 <div *ngIf="protoPayloadType" fxLayout="column"> 49 <div *ngIf="protoPayloadType" fxLayout="column">
50 - <mat-form-field fxFlex>  
51 - <mat-label translate>device-profile.telemetry-proto-schema</mat-label>  
52 - <textarea matInput required formControlName="deviceTelemetryProtoSchema" rows="5"></textarea> 50 + <ng-container>
  51 + <tb-protobuf-content
  52 + fxFlex
  53 + formControlName="deviceTelemetryProtoSchema"
  54 + label="{{ 'device-profile.telemetry-proto-schema' | translate }}"
  55 + [fillHeight]="true">
  56 + </tb-protobuf-content>
53 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')"> 57 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
54 {{ 'device-profile.telemetry-proto-schema-required' | translate}} 58 {{ 'device-profile.telemetry-proto-schema-required' | translate}}
55 </mat-error> 59 </mat-error>
56 - </mat-form-field>  
57 - <mat-form-field fxFlex>  
58 - <mat-label translate>device-profile.attributes-proto-schema</mat-label>  
59 - <textarea matInput required formControlName="deviceAttributesProtoSchema" rows="5"></textarea> 60 + </ng-container>
  61 + <ng-container>
  62 + <tb-protobuf-content
  63 + fxFlex
  64 + formControlName="deviceAttributesProtoSchema"
  65 + label="{{ 'device-profile.attributes-proto-schema' | translate }}"
  66 + [fillHeight]="true">
  67 + </tb-protobuf-content>
60 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')"> 68 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
61 {{ 'device-profile.attributes-proto-schema-required' | translate}} 69 {{ 'device-profile.attributes-proto-schema-required' | translate}}
62 </mat-error> 70 </mat-error>
63 - </mat-form-field>  
64 - <mat-form-field style="padding-bottom: 20px" fxFlex>  
65 - <mat-label translate>device-profile.rpc-request-proto-schema</mat-label>  
66 - <textarea matInput required formControlName="deviceRpcRequestProtoSchema" rows="5"></textarea> 71 + </ng-container>
  72 + <ng-container>
  73 + <tb-protobuf-content
  74 + fxFlex
  75 + formControlName="deviceRpcRequestProtoSchema"
  76 + label="{{ 'device-profile.rpc-request-proto-schema' | translate }}"
  77 + [fillHeight]="true">
  78 + </tb-protobuf-content>
67 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')"> 79 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
68 {{ 'device-profile.rpc-request-proto-schema-required' | translate}} 80 {{ 'device-profile.rpc-request-proto-schema-required' | translate}}
69 </mat-error> 81 </mat-error>
70 - <mat-hint class="tb-hint" translate>device-profile.rpc-request-proto-schema-hint</mat-hint>  
71 - </mat-form-field>  
72 - <mat-form-field fxFlex>  
73 - <mat-label translate>device-profile.rpc-response-proto-schema</mat-label>  
74 - <textarea matInput required formControlName="deviceRpcResponseProtoSchema" rows="5"></textarea> 82 + </ng-container>
  83 + <ng-container>
  84 + <tb-protobuf-content
  85 + fxFlex
  86 + formControlName="deviceRpcResponseProtoSchema"
  87 + label="{{ 'device-profile.rpc-response-proto-schema' | translate }}"
  88 + [fillHeight]="true">
  89 + </tb-protobuf-content>
75 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')"> 90 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
76 {{ 'device-profile.rpc-response-proto-schema-required' | translate}} 91 {{ 'device-profile.rpc-response-proto-schema-required' | translate}}
77 </mat-error> 92 </mat-error>
78 - </mat-form-field> 93 + </ng-container>
79 </div> 94 </div>
80 </div> 95 </div>
81 </fieldset> 96 </fieldset>
@@ -86,35 +86,50 @@ @@ -86,35 +86,50 @@
86 </div> 86 </div>
87 </div> 87 </div>
88 <div *ngIf="protoPayloadType" fxLayout="column"> 88 <div *ngIf="protoPayloadType" fxLayout="column">
89 - <mat-form-field fxFlex>  
90 - <mat-label translate>device-profile.telemetry-proto-schema</mat-label>  
91 - <textarea matInput required formControlName="deviceTelemetryProtoSchema" rows="5"></textarea> 89 + <ng-container>
  90 + <tb-protobuf-content
  91 + fxFlex
  92 + formControlName="deviceTelemetryProtoSchema"
  93 + label="{{ 'device-profile.telemetry-proto-schema' | translate }}"
  94 + [fillHeight]="true">
  95 + </tb-protobuf-content>
92 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')"> 96 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
93 {{ 'device-profile.telemetry-proto-schema-required' | translate}} 97 {{ 'device-profile.telemetry-proto-schema-required' | translate}}
94 </mat-error> 98 </mat-error>
95 - </mat-form-field>  
96 - <mat-form-field fxFlex>  
97 - <mat-label translate>device-profile.attributes-proto-schema</mat-label>  
98 - <textarea matInput required formControlName="deviceAttributesProtoSchema" rows="5"></textarea> 99 + </ng-container>
  100 + <ng-container>
  101 + <tb-protobuf-content
  102 + fxFlex
  103 + formControlName="deviceAttributesProtoSchema"
  104 + label="{{ 'device-profile.attributes-proto-schema' | translate }}"
  105 + [fillHeight]="true">
  106 + </tb-protobuf-content>
99 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')"> 107 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
100 {{ 'device-profile.attributes-proto-schema-required' | translate}} 108 {{ 'device-profile.attributes-proto-schema-required' | translate}}
101 </mat-error> 109 </mat-error>
102 - </mat-form-field>  
103 - <mat-form-field style="padding-bottom: 20px" fxFlex>  
104 - <mat-label translate>device-profile.rpc-request-proto-schema</mat-label>  
105 - <textarea matInput required formControlName="deviceRpcRequestProtoSchema" rows="5"></textarea> 110 + </ng-container>
  111 + <ng-container>
  112 + <tb-protobuf-content
  113 + fxFlex
  114 + formControlName="deviceRpcRequestProtoSchema"
  115 + label="{{ 'device-profile.rpc-request-proto-schema' | translate }}"
  116 + [fillHeight]="true">
  117 + </tb-protobuf-content>
106 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')"> 118 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
107 {{ 'device-profile.rpc-request-proto-schema-required' | translate}} 119 {{ 'device-profile.rpc-request-proto-schema-required' | translate}}
108 </mat-error> 120 </mat-error>
109 - <mat-hint class="tb-hint" translate>device-profile.rpc-request-proto-schema-hint</mat-hint>  
110 - </mat-form-field>  
111 - <mat-form-field fxFlex>  
112 - <mat-label translate>device-profile.rpc-response-proto-schema</mat-label>  
113 - <textarea matInput required formControlName="deviceRpcResponseProtoSchema" rows="5"></textarea> 121 + </ng-container>
  122 + <ng-container>
  123 + <tb-protobuf-content
  124 + fxFlex
  125 + formControlName="deviceRpcResponseProtoSchema"
  126 + label="{{ 'device-profile.rpc-response-proto-schema' | translate }}"
  127 + [fillHeight]="true">
  128 + </tb-protobuf-content>
114 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')"> 129 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
115 {{ 'device-profile.rpc-response-proto-schema-required' | translate}} 130 {{ 'device-profile.rpc-response-proto-schema-required' | translate}}
116 </mat-error> 131 </mat-error>
117 - </mat-form-field> 132 + </ng-container>
118 </div> 133 </div>
119 </div> 134 </div>
120 </fieldset> 135 </fieldset>
1 -<!--  
2 -  
3 - Copyright © 2016-2021 The Thingsboard Authors  
4 -  
5 - Licensed under the Apache License, Version 2.0 (the "License");  
6 - you may not use this file except in compliance with the License.  
7 - You may obtain a copy of the License at  
8 -  
9 - http://www.apache.org/licenses/LICENSE-2.0  
10 -  
11 - Unless required by applicable law or agreed to in writing, software  
12 - distributed under the License is distributed on an "AS IS" BASIS,  
13 - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
14 - See the License for the specific language governing permissions and  
15 - limitations under the License.  
16 -  
17 --->  
18 -<form [formGroup]="legendConfigForm">  
19 - <fieldset [disabled]="(isLoading$ | async)">  
20 - <div class="mat-content" style="height: 100%;">  
21 - <div class="mat-padding">  
22 - <section fxLayout="column">  
23 - <mat-form-field>  
24 - <mat-label translate>legend.direction</mat-label>  
25 - <mat-select matInput formControlName="direction" style="min-width: 150px;">  
26 - <mat-option *ngFor="let direction of legendDirections" [value]="direction">  
27 - {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }}  
28 - </mat-option>  
29 - </mat-select>  
30 - </mat-form-field>  
31 - <mat-form-field>  
32 - <mat-label translate>legend.position</mat-label>  
33 - <mat-select matInput formControlName="position" style="min-width: 150px;">  
34 - <mat-option *ngFor="let pos of legendPositions" [value]="pos"  
35 - [disabled]="legendConfigForm.get('direction').value === legendDirection.row &&  
36 - (pos === legendPosition.left || pos === legendPosition.right)">  
37 - {{ legendPositionTranslations.get(legendPosition[pos]) | translate }}  
38 - </mat-option>  
39 - </mat-select>  
40 - </mat-form-field>  
41 - <mat-checkbox formControlName="sortDataKeys">  
42 - {{ 'legend.sort-legend' | translate }}  
43 - </mat-checkbox>  
44 - <mat-checkbox formControlName="showMin">  
45 - {{ 'legend.show-min' | translate }}  
46 - </mat-checkbox>  
47 - <mat-checkbox formControlName="showMax">  
48 - {{ 'legend.show-max' | translate }}  
49 - </mat-checkbox>  
50 - <mat-checkbox formControlName="showAvg">  
51 - {{ 'legend.show-avg' | translate }}  
52 - </mat-checkbox>  
53 - <mat-checkbox formControlName="showTotal">  
54 - {{ 'legend.show-total' | translate }}  
55 - </mat-checkbox>  
56 - </section>  
57 - </div>  
58 - </div>  
59 - </fieldset>  
60 -</form>  
1 -///  
2 -/// Copyright © 2016-2021 The Thingsboard Authors  
3 -///  
4 -/// Licensed under the Apache License, Version 2.0 (the "License");  
5 -/// you may not use this file except in compliance with the License.  
6 -/// You may obtain a copy of the License at  
7 -///  
8 -/// http://www.apache.org/licenses/LICENSE-2.0  
9 -///  
10 -/// Unless required by applicable law or agreed to in writing, software  
11 -/// distributed under the License is distributed on an "AS IS" BASIS,  
12 -/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  
13 -/// See the License for the specific language governing permissions and  
14 -/// limitations under the License.  
15 -///  
16 -  
17 -import { Component, Inject, InjectionToken, OnInit, ViewContainerRef } from '@angular/core';  
18 -import { OverlayRef } from '@angular/cdk/overlay';  
19 -import { PageComponent } from '@shared/components/page.component';  
20 -import { Store } from '@ngrx/store';  
21 -import { AppState } from '@core/core.state';  
22 -import { FormBuilder, FormGroup } from '@angular/forms';  
23 -import {  
24 - LegendConfig,  
25 - LegendDirection,  
26 - legendDirectionTranslationMap,  
27 - LegendPosition,  
28 - legendPositionTranslationMap  
29 -} from '@shared/models/widget.models';  
30 -  
31 -export const LEGEND_CONFIG_PANEL_DATA = new InjectionToken<any>('LegendConfigPanelData');  
32 -  
33 -export interface LegendConfigPanelData {  
34 - legendConfig: LegendConfig;  
35 - legendConfigUpdated: (legendConfig: LegendConfig) => void;  
36 -}  
37 -  
38 -@Component({  
39 - selector: 'tb-legend-config-panel',  
40 - templateUrl: './legend-config-panel.component.html',  
41 - styleUrls: ['./legend-config-panel.component.scss']  
42 -})  
43 -export class LegendConfigPanelComponent extends PageComponent implements OnInit {  
44 -  
45 - legendConfigForm: FormGroup;  
46 -  
47 - legendDirection = LegendDirection;  
48 -  
49 - legendDirections = Object.keys(LegendDirection);  
50 -  
51 - legendDirectionTranslations = legendDirectionTranslationMap;  
52 -  
53 - legendPosition = LegendPosition;  
54 -  
55 - legendPositions = Object.keys(LegendPosition);  
56 -  
57 - legendPositionTranslations = legendPositionTranslationMap;  
58 -  
59 - constructor(@Inject(LEGEND_CONFIG_PANEL_DATA) public data: LegendConfigPanelData,  
60 - public overlayRef: OverlayRef,  
61 - protected store: Store<AppState>,  
62 - public fb: FormBuilder,  
63 - public viewContainerRef: ViewContainerRef) {  
64 - super(store);  
65 - }  
66 -  
67 - ngOnInit(): void {  
68 - this.legendConfigForm = this.fb.group({  
69 - direction: [this.data.legendConfig.direction, []],  
70 - position: [this.data.legendConfig.position, []],  
71 - sortDataKeys: [this.data.legendConfig.sortDataKeys, []],  
72 - showMin: [this.data.legendConfig.showMin, []],  
73 - showMax: [this.data.legendConfig.showMax, []],  
74 - showAvg: [this.data.legendConfig.showAvg, []],  
75 - showTotal: [this.data.legendConfig.showTotal, []]  
76 - });  
77 - this.legendConfigForm.get('direction').valueChanges.subscribe((direction: LegendDirection) => {  
78 - this.onDirectionChanged(direction);  
79 - });  
80 - this.onDirectionChanged(this.data.legendConfig.direction);  
81 - this.legendConfigForm.valueChanges.subscribe(() => {  
82 - this.update();  
83 - });  
84 - }  
85 -  
86 - private onDirectionChanged(direction: LegendDirection) {  
87 - if (direction === LegendDirection.row) {  
88 - let position: LegendPosition = this.legendConfigForm.get('position').value;  
89 - if (position !== LegendPosition.bottom && position !== LegendPosition.top) {  
90 - position = LegendPosition.bottom;  
91 - }  
92 - this.legendConfigForm.patchValue(  
93 - {  
94 - position  
95 - }, {emitEvent: false}  
96 - );  
97 - }  
98 - }  
99 -  
100 - update() {  
101 - const newLegendConfig: LegendConfig = this.legendConfigForm.value;  
102 - this.data.legendConfigUpdated(newLegendConfig);  
103 - }  
104 -  
105 -}  
@@ -15,9 +15,43 @@ @@ -15,9 +15,43 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<button cdkOverlayOrigin #legendConfigPanelOrigin="cdkOverlayOrigin" [disabled]="disabled"  
19 - type="button"  
20 - mat-button mat-raised-button color="primary" (click)="openEditMode()">  
21 - <mat-icon class="material-icons">toc</mat-icon>  
22 - <span translate>legend.settings</span>  
23 -</button> 18 +<form [formGroup]="legendConfigForm">
  19 + <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
  20 + fxLayoutGap="8px">
  21 + <mat-form-field fxFlex>
  22 + <mat-label translate>legend.direction</mat-label>
  23 + <mat-select matInput formControlName="direction">
  24 + <mat-option *ngFor="let direction of legendDirections" [value]="direction">
  25 + {{ legendDirectionTranslations.get(legendDirection[direction]) | translate }}
  26 + </mat-option>
  27 + </mat-select>
  28 + </mat-form-field>
  29 + <mat-form-field fxFlex>
  30 + <mat-label translate>legend.position</mat-label>
  31 + <mat-select matInput formControlName="position">
  32 + <mat-option *ngFor="let pos of legendPositions" [value]="pos"
  33 + [disabled]="legendConfigForm.get('direction').value === legendDirection.row &&
  34 + (pos === legendPosition.left || pos === legendPosition.right)">
  35 + {{ legendPositionTranslations.get(legendPosition[pos]) | translate }}
  36 + </mat-option>
  37 + </mat-select>
  38 + </mat-form-field>
  39 + </div>
  40 + <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row wrap" fxLayoutAlign="space-between center" fxLayoutGap="8px">
  41 + <mat-checkbox formControlName="showMin" fxFlex="48">
  42 + {{ 'legend.show-min' | translate }}
  43 + </mat-checkbox>
  44 + <mat-checkbox formControlName="showMax" fxFlex="48">
  45 + {{ 'legend.show-max' | translate }}
  46 + </mat-checkbox>
  47 + <mat-checkbox formControlName="showAvg" fxFlex="48">
  48 + {{ 'legend.show-avg' | translate }}
  49 + </mat-checkbox>
  50 + <mat-checkbox formControlName="showTotal" fxFlex="48">
  51 + {{ 'legend.show-total' | translate }}
  52 + </mat-checkbox>
  53 + <mat-checkbox formControlName="sortDataKeys" fxFlex="48">
  54 + {{ 'legend.sort-legend' | translate }}
  55 + </mat-checkbox>
  56 + </div>
  57 +</form>
@@ -14,32 +14,17 @@ @@ -14,32 +14,17 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
  17 +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
  18 +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
  19 +import { isDefined } from '@core/utils';
17 import { 20 import {
18 - Component,  
19 - forwardRef,  
20 - Inject,  
21 - Injector,  
22 - Input,  
23 - OnDestroy,  
24 - OnInit,  
25 - StaticProvider,  
26 - ViewChild,  
27 - ViewContainerRef  
28 -} from '@angular/core';  
29 -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';  
30 -import { DOCUMENT } from '@angular/common';  
31 -import { CdkOverlayOrigin, ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay';  
32 -import { ComponentPortal } from '@angular/cdk/portal';  
33 -import { MediaBreakpoints } from '@shared/models/constants';  
34 -import { BreakpointObserver } from '@angular/cdk/layout';  
35 -import { WINDOW } from '@core/services/window.service';  
36 -import { deepClone } from '@core/utils';  
37 -import { LegendConfig } from '@shared/models/widget.models';  
38 -import {  
39 - LEGEND_CONFIG_PANEL_DATA,  
40 - LegendConfigPanelComponent,  
41 - LegendConfigPanelData  
42 -} from '@home/components/widget/legend-config-panel.component'; 21 + LegendConfig,
  22 + LegendDirection,
  23 + legendDirectionTranslationMap,
  24 + LegendPosition,
  25 + legendPositionTranslationMap
  26 +} from '@shared/models/widget.models';
  27 +import { Subscription } from 'rxjs';
43 28
44 // @dynamic 29 // @dynamic
45 @Component({ 30 @Component({
@@ -58,105 +43,60 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc @@ -58,105 +43,60 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
58 43
59 @Input() disabled: boolean; 44 @Input() disabled: boolean;
60 45
61 - @ViewChild('legendConfigPanelOrigin') legendConfigPanelOrigin: CdkOverlayOrigin;  
62 -  
63 - innerValue: LegendConfig; 46 + legendConfigForm: FormGroup;
  47 + legendDirection = LegendDirection;
  48 + legendDirections = Object.keys(LegendDirection);
  49 + legendDirectionTranslations = legendDirectionTranslationMap;
  50 + legendPosition = LegendPosition;
  51 + legendPositions = Object.keys(LegendPosition);
  52 + legendPositionTranslations = legendPositionTranslationMap;
64 53
  54 + private legendSettingsFormChanges$: Subscription;
  55 + private legendSettingsFormDirectionChanges$: Subscription;
65 private propagateChange = (_: any) => {}; 56 private propagateChange = (_: any) => {};
66 57
67 - constructor(private overlay: Overlay,  
68 - public viewContainerRef: ViewContainerRef,  
69 - public breakpointObserver: BreakpointObserver,  
70 - @Inject(DOCUMENT) private document: Document,  
71 - @Inject(WINDOW) private window: Window) { 58 + constructor(private fb: FormBuilder) {
72 } 59 }
73 60
74 ngOnInit(): void { 61 ngOnInit(): void {
  62 + this.legendConfigForm = this.fb.group({
  63 + direction: [null, []],
  64 + position: [null, []],
  65 + sortDataKeys: [null, []],
  66 + showMin: [null, []],
  67 + showMax: [null, []],
  68 + showAvg: [null, []],
  69 + showTotal: [null, []]
  70 + });
  71 + this.legendSettingsFormDirectionChanges$ = this.legendConfigForm.get('direction').valueChanges
  72 + .subscribe((direction: LegendDirection) => {
  73 + this.onDirectionChanged(direction);
  74 + });
  75 + this.legendSettingsFormChanges$ = this.legendConfigForm.valueChanges.subscribe(
  76 + () => this.legendConfigUpdated()
  77 + );
75 } 78 }
76 79
77 - ngOnDestroy(): void {  
78 - }  
79 -  
80 - openEditMode() {  
81 - if (this.disabled) {  
82 - return;  
83 - }  
84 - const isGtSm = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']);  
85 - const position = this.overlay.position();  
86 - const config = new OverlayConfig({  
87 - panelClass: 'tb-legend-config-panel',  
88 - backdropClass: 'cdk-overlay-transparent-backdrop',  
89 - hasBackdrop: isGtSm,  
90 - });  
91 - if (isGtSm) {  
92 - config.minWidth = '220px';  
93 - config.maxHeight = '300px';  
94 - const panelHeight = 220;  
95 - const panelWidth = 220;  
96 - const el = this.legendConfigPanelOrigin.elementRef.nativeElement;  
97 - const offset = el.getBoundingClientRect();  
98 - const scrollTop = this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;  
99 - const scrollLeft = this.window.pageXOffset || this.document.documentElement.scrollLeft || this.document.body.scrollLeft || 0;  
100 - const bottomY = offset.bottom - scrollTop;  
101 - const leftX = offset.left - scrollLeft;  
102 - let originX;  
103 - let originY;  
104 - let overlayX;  
105 - let overlayY;  
106 - const wHeight = this.document.documentElement.clientHeight;  
107 - const wWidth = this.document.documentElement.clientWidth;  
108 - if (bottomY + panelHeight > wHeight) {  
109 - originY = 'top';  
110 - overlayY = 'bottom';  
111 - } else {  
112 - originY = 'bottom';  
113 - overlayY = 'top'; 80 + private onDirectionChanged(direction: LegendDirection) {
  81 + if (direction === LegendDirection.row) {
  82 + let position: LegendPosition = this.legendConfigForm.get('position').value;
  83 + if (position !== LegendPosition.bottom && position !== LegendPosition.top) {
  84 + position = LegendPosition.bottom;
114 } 85 }
115 - if (leftX + panelWidth > wWidth) {  
116 - originX = 'end';  
117 - overlayX = 'end';  
118 - } else {  
119 - originX = 'start';  
120 - overlayX = 'start';  
121 - }  
122 - const connectedPosition: ConnectedPosition = {  
123 - originX,  
124 - originY,  
125 - overlayX,  
126 - overlayY  
127 - };  
128 - config.positionStrategy = position.flexibleConnectedTo(this.legendConfigPanelOrigin.elementRef)  
129 - .withPositions([connectedPosition]);  
130 - } else {  
131 - config.minWidth = '100%';  
132 - config.minHeight = '100%';  
133 - config.positionStrategy = position.global().top('0%').left('0%')  
134 - .right('0%').bottom('0%'); 86 + this.legendConfigForm.patchValue({position}, {emitEvent: false}
  87 + );
135 } 88 }
136 -  
137 - const overlayRef = this.overlay.create(config);  
138 -  
139 - overlayRef.backdropClick().subscribe(() => {  
140 - overlayRef.dispose();  
141 - });  
142 -  
143 - const injector = this._createLegendConfigPanelInjector(  
144 - overlayRef,  
145 - {  
146 - legendConfig: deepClone(this.innerValue),  
147 - legendConfigUpdated: this.legendConfigUpdated.bind(this)  
148 - }  
149 - );  
150 -  
151 - overlayRef.attach(new ComponentPortal(LegendConfigPanelComponent, this.viewContainerRef, injector));  
152 } 89 }
153 90
154 - private _createLegendConfigPanelInjector(overlayRef: OverlayRef, data: LegendConfigPanelData): Injector {  
155 - const providers: StaticProvider[] = [  
156 - {provide: LEGEND_CONFIG_PANEL_DATA, useValue: data},  
157 - {provide: OverlayRef, useValue: overlayRef}  
158 - ];  
159 - return Injector.create({parent: this.viewContainerRef.injector, providers}); 91 + ngOnDestroy(): void {
  92 + if (this.legendSettingsFormDirectionChanges$) {
  93 + this.legendSettingsFormDirectionChanges$.unsubscribe();
  94 + this.legendSettingsFormDirectionChanges$ = null;
  95 + }
  96 + if (this.legendSettingsFormChanges$) {
  97 + this.legendSettingsFormChanges$.unsubscribe();
  98 + this.legendSettingsFormChanges$ = null;
  99 + }
160 } 100 }
161 101
162 registerOnChange(fn: any): void { 102 registerOnChange(fn: any): void {
@@ -168,14 +108,29 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc @@ -168,14 +108,29 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
168 108
169 setDisabledState(isDisabled: boolean): void { 109 setDisabledState(isDisabled: boolean): void {
170 this.disabled = isDisabled; 110 this.disabled = isDisabled;
  111 + if (this.disabled) {
  112 + this.legendConfigForm.disable({emitEvent: false});
  113 + } else {
  114 + this.legendConfigForm.enable({emitEvent: false});
  115 + }
171 } 116 }
172 117
173 - writeValue(obj: LegendConfig): void {  
174 - this.innerValue = obj; 118 + writeValue(legendConfig: LegendConfig): void {
  119 + if (legendConfig) {
  120 + this.legendConfigForm.patchValue({
  121 + direction: legendConfig.direction,
  122 + position: legendConfig.position,
  123 + sortDataKeys: isDefined(legendConfig.sortDataKeys) ? legendConfig.sortDataKeys : false,
  124 + showMin: isDefined(legendConfig.showMin) ? legendConfig.showMin : false,
  125 + showMax: isDefined(legendConfig.showMax) ? legendConfig.showMax : false,
  126 + showAvg: isDefined(legendConfig.showAvg) ? legendConfig.showAvg : false,
  127 + showTotal: isDefined(legendConfig.showTotal) ? legendConfig.showTotal : false
  128 + }, {emitEvent: false});
  129 + }
  130 + this.onDirectionChanged(legendConfig.direction);
175 } 131 }
176 132
177 - private legendConfigUpdated(legendConfig: LegendConfig) {  
178 - this.innerValue = legendConfig;  
179 - this.propagateChange(this.innerValue); 133 + private legendConfigUpdated() {
  134 + this.propagateChange(this.legendConfigForm.value);
180 } 135 }
181 } 136 }
@@ -82,222 +82,246 @@ @@ -82,222 +82,246 @@
82 </mat-checkbox> 82 </mat-checkbox>
83 </div> 83 </div>
84 </div> 84 </div>
85 - <mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&  
86 - widgetType !== widgetTypes.alarm &&  
87 - modelValue?.isDataEnabled" [expanded]="true">  
88 - <mat-expansion-panel-header>  
89 - <mat-panel-title fxLayout="column">  
90 - <div class="tb-panel-title" translate>widget-config.datasources</div>  
91 - <div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1"  
92 - class="tb-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div>  
93 - </mat-panel-title>  
94 - </mat-expansion-panel-header>  
95 - <div *ngIf="datasourcesFormArray().length === 0; else datasourcesTemplate">  
96 - <span translate fxLayoutAlign="center center"  
97 - class="tb-prompt">datasource.add-datasource-prompt</span>  
98 - </div>  
99 - <ng-template #datasourcesTemplate>  
100 - <div fxFlex fxLayout="row" fxLayoutAlign="start center">  
101 - <span style="width: 60px;"></span>  
102 - <div fxFlex fxLayout="row" fxLayoutAlign="start center"  
103 - style="padding: 0 0 0 10px; margin: 5px;">  
104 - <span translate style="min-width: 120px;">widget-config.datasource-type</span>  
105 - <span fxHide fxShow.gt-sm translate fxFlex  
106 - style="padding-left: 10px;">widget-config.datasource-parameters</span>  
107 - <span style="min-width: 40px;"></span>  
108 - </div> 85 + <mat-accordion multi>
  86 + <mat-expansion-panel class="tb-datasources" *ngIf="widgetType !== widgetTypes.rpc &&
  87 + widgetType !== widgetTypes.alarm &&
  88 + modelValue?.isDataEnabled" [expanded]="true">
  89 + <mat-expansion-panel-header>
  90 + <mat-panel-title fxLayout="column">
  91 + <div class="tb-panel-title" translate>widget-config.datasources</div>
  92 + <div *ngIf="modelValue?.typeParameters && modelValue?.typeParameters.maxDatasources > -1"
  93 + class="tb-panel-hint">{{ 'widget-config.maximum-datasources' | translate:{count: modelValue?.typeParameters.maxDatasources} }}</div>
  94 + </mat-panel-title>
  95 + </mat-expansion-panel-header>
  96 + <div *ngIf="datasourcesFormArray().length === 0; else datasourcesTemplate">
  97 + <span translate fxLayoutAlign="center center"
  98 + class="tb-prompt">datasource.add-datasource-prompt</span>
109 </div> 99 </div>
110 - <div style="overflow: auto; padding-bottom: 15px;">  
111 - <mat-list dndDropzone dndEffectAllowed="move"  
112 - (dndDrop)="onDatasourceDrop($event)"  
113 - [dndDisableIf]="disabled" formArrayName="datasources">  
114 - <mat-list-item dndPlaceholderRef  
115 - class="dndPlaceholder">  
116 - </mat-list-item>  
117 - <mat-list-item *ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;"  
118 - [dndDraggable]="datasourceControl.value"  
119 - (dndMoved)="dndDatasourceMoved($index)"  
120 - [dndDisableIf]="disabled"  
121 - dndEffectAllowed="move">  
122 - <div fxFlex fxLayout="row" fxLayoutAlign="start center">  
123 - <div style="width: 60px;">  
124 - <button *ngIf="!disabled" mat-icon-button color="primary"  
125 - class="handle"  
126 - style="min-width: 40px; margin: 0"  
127 - dndHandle  
128 - matTooltip="{{ 'action.drag' | translate }}"  
129 - matTooltipPosition="above">  
130 - <mat-icon>drag_handle</mat-icon>  
131 - </button>  
132 - <span>{{$index + 1}}.</span>  
133 - </div>  
134 - <div class="mat-elevation-z4" fxFlex  
135 - fxLayout="row"  
136 - fxLayoutAlign="start center"  
137 - style="padding: 0 0 0 10px; margin: 5px;">  
138 - <section fxFlex  
139 - fxLayout="column"  
140 - fxLayoutAlign="center"  
141 - fxLayout.gt-sm="row"  
142 - fxLayoutAlign.gt-sm="start center">  
143 - <mat-form-field class="tb-datasource-type">  
144 - <mat-select [formControl]="datasourceControl.get('type')">  
145 - <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">  
146 - {{ datasourceTypesTranslations.get(datasourceType) | translate }}  
147 - </mat-option>  
148 - </mat-select>  
149 - </mat-form-field>  
150 - <section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">  
151 - <ng-template [ngSwitchCase]="datasourceType.function">  
152 - <mat-form-field floatLabel="always"  
153 - class="tb-datasource-name" style="min-width: 200px;">  
154 - <mat-label></mat-label>  
155 - <input matInput  
156 - placeholder="{{ 'datasource.label' | translate }}"  
157 - [formControl]="datasourceControl.get('name')">  
158 - </mat-form-field>  
159 - </ng-template>  
160 - <ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity ||  
161 - datasourceControl.get('type').value === datasourceType.entityCount ? datasourceControl.get('type').value : ''">  
162 - <tb-entity-alias-select  
163 - [showLabel]="true"  
164 - [tbRequired]="true"  
165 - [aliasController]="aliasController"  
166 - [formControl]="datasourceControl.get('entityAliasId')"  
167 - [callbacks]="widgetConfigCallbacks">  
168 - </tb-entity-alias-select>  
169 - <tb-filter-select  
170 - [showLabel]="true"  
171 - [aliasController]="aliasController"  
172 - [formControl]="datasourceControl.get('filterId')"  
173 - [callbacks]="widgetConfigCallbacks">  
174 - </tb-filter-select>  
175 - <mat-form-field *ngIf="datasourceControl.get('type').value === datasourceType.entityCount"  
176 - floatLabel="always"  
177 - class="tb-datasource-name no-border-top" style="min-width: 200px;">  
178 - <mat-label></mat-label>  
179 - <input matInput  
180 - placeholder="{{ 'datasource.label' | translate }}"  
181 - [formControl]="datasourceControl.get('name')">  
182 - </mat-form-field>  
183 - </ng-template> 100 + <ng-template #datasourcesTemplate>
  101 + <div fxFlex fxLayout="row" fxLayoutAlign="start center">
  102 + <span style="width: 60px;"></span>
  103 + <div fxFlex fxLayout="row" fxLayoutAlign="start center"
  104 + style="padding: 0 0 0 10px; margin: 5px;">
  105 + <span translate style="min-width: 120px;">widget-config.datasource-type</span>
  106 + <span fxHide fxShow.gt-sm translate fxFlex
  107 + style="padding-left: 10px;">widget-config.datasource-parameters</span>
  108 + <span style="min-width: 40px;"></span>
  109 + </div>
  110 + </div>
  111 + <div style="overflow: auto; padding-bottom: 15px;">
  112 + <mat-list dndDropzone dndEffectAllowed="move"
  113 + (dndDrop)="onDatasourceDrop($event)"
  114 + [dndDisableIf]="disabled" formArrayName="datasources">
  115 + <mat-list-item dndPlaceholderRef
  116 + class="dndPlaceholder">
  117 + </mat-list-item>
  118 + <mat-list-item *ngFor="let datasourceControl of datasourcesFormArray().controls; let $index = index;"
  119 + [dndDraggable]="datasourceControl.value"
  120 + (dndMoved)="dndDatasourceMoved($index)"
  121 + [dndDisableIf]="disabled"
  122 + dndEffectAllowed="move">
  123 + <div fxFlex fxLayout="row" fxLayoutAlign="start center">
  124 + <div style="width: 60px;">
  125 + <button *ngIf="!disabled" mat-icon-button color="primary"
  126 + class="handle"
  127 + style="min-width: 40px; margin: 0"
  128 + dndHandle
  129 + matTooltip="{{ 'action.drag' | translate }}"
  130 + matTooltipPosition="above">
  131 + <mat-icon>drag_handle</mat-icon>
  132 + </button>
  133 + <span>{{$index + 1}}.</span>
  134 + </div>
  135 + <div class="mat-elevation-z4" fxFlex
  136 + fxLayout="row"
  137 + fxLayoutAlign="start center"
  138 + style="padding: 0 0 0 10px; margin: 5px;">
  139 + <section fxFlex
  140 + fxLayout="column"
  141 + fxLayoutAlign="center"
  142 + fxLayout.gt-sm="row"
  143 + fxLayoutAlign.gt-sm="start center">
  144 + <mat-form-field class="tb-datasource-type">
  145 + <mat-select [formControl]="datasourceControl.get('type')">
  146 + <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
  147 + {{ datasourceTypesTranslations.get(datasourceType) | translate }}
  148 + </mat-option>
  149 + </mat-select>
  150 + </mat-form-field>
  151 + <section fxLayout="column" class="tb-datasource" [ngSwitch]="datasourceControl.get('type').value">
  152 + <ng-template [ngSwitchCase]="datasourceType.function">
  153 + <mat-form-field floatLabel="always"
  154 + class="tb-datasource-name" style="min-width: 200px;">
  155 + <mat-label></mat-label>
  156 + <input matInput
  157 + placeholder="{{ 'datasource.label' | translate }}"
  158 + [formControl]="datasourceControl.get('name')">
  159 + </mat-form-field>
  160 + </ng-template>
  161 + <ng-template [ngSwitchCase]="datasourceControl.get('type').value === datasourceType.entity ||
  162 + datasourceControl.get('type').value === datasourceType.entityCount ? datasourceControl.get('type').value : ''">
  163 + <tb-entity-alias-select
  164 + [showLabel]="true"
  165 + [tbRequired]="true"
  166 + [aliasController]="aliasController"
  167 + [formControl]="datasourceControl.get('entityAliasId')"
  168 + [callbacks]="widgetConfigCallbacks">
  169 + </tb-entity-alias-select>
  170 + <tb-filter-select
  171 + [showLabel]="true"
  172 + [aliasController]="aliasController"
  173 + [formControl]="datasourceControl.get('filterId')"
  174 + [callbacks]="widgetConfigCallbacks">
  175 + </tb-filter-select>
  176 + <mat-form-field *ngIf="datasourceControl.get('type').value === datasourceType.entityCount"
  177 + floatLabel="always"
  178 + class="tb-datasource-name no-border-top" style="min-width: 200px;">
  179 + <mat-label></mat-label>
  180 + <input matInput
  181 + placeholder="{{ 'datasource.label' | translate }}"
  182 + [formControl]="datasourceControl.get('name')">
  183 + </mat-form-field>
  184 + </ng-template>
  185 + </section>
  186 + <tb-data-keys class="tb-data-keys" fxFlex
  187 + [widgetType]="widgetType"
  188 + [datasourceType]="datasourceControl.get('type').value"
  189 + [maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"
  190 + [optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"
  191 + [aliasController]="aliasController"
  192 + [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
  193 + [callbacks]="widgetConfigCallbacks"
  194 + [entityAliasId]="datasourceControl.get('entityAliasId').value"
  195 + [formControl]="datasourceControl.get('dataKeys')">
  196 + </tb-data-keys>
184 </section> 197 </section>
185 - <tb-data-keys class="tb-data-keys" fxFlex  
186 - [widgetType]="widgetType"  
187 - [datasourceType]="datasourceControl.get('type').value"  
188 - [maxDataKeys]="modelValue?.typeParameters?.maxDataKeys"  
189 - [optDataKeys]="modelValue?.typeParameters?.dataKeysOptional"  
190 - [aliasController]="aliasController"  
191 - [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"  
192 - [callbacks]="widgetConfigCallbacks"  
193 - [entityAliasId]="datasourceControl.get('entityAliasId').value"  
194 - [formControl]="datasourceControl.get('dataKeys')">  
195 - </tb-data-keys>  
196 - </section>  
197 - <button [disabled]="isLoading$ | async"  
198 - type="button"  
199 - mat-icon-button color="primary"  
200 - style="min-width: 40px;"  
201 - (click)="removeDatasource($index)"  
202 - matTooltip="{{ 'widget-config.remove-datasource' | translate }}"  
203 - matTooltipPosition="above">  
204 - <mat-icon>close</mat-icon>  
205 - </button> 198 + <button [disabled]="isLoading$ | async"
  199 + type="button"
  200 + mat-icon-button color="primary"
  201 + style="min-width: 40px;"
  202 + (click)="removeDatasource($index)"
  203 + matTooltip="{{ 'widget-config.remove-datasource' | translate }}"
  204 + matTooltipPosition="above">
  205 + <mat-icon>close</mat-icon>
  206 + </button>
  207 + </div>
206 </div> 208 </div>
207 - </div>  
208 - </mat-list-item>  
209 - </mat-list> 209 + </mat-list-item>
  210 + </mat-list>
  211 + </div>
  212 + </ng-template>
  213 + <div fxFlex fxLayout="row" fxLayoutAlign="start center">
  214 + <button [disabled]="isLoading$ | async"
  215 + type="button"
  216 + mat-raised-button color="primary"
  217 + [fxShow]="modelValue?.typeParameters &&
  218 + (modelValue?.typeParameters.maxDatasources == -1 || datasourcesFormArray().controls.length < modelValue?.typeParameters.maxDatasources)"
  219 + (click)="addDatasource()"
  220 + matTooltip="{{ 'widget-config.add-datasource' | translate }}"
  221 + matTooltipPosition="above">
  222 + <mat-icon>add</mat-icon>
  223 + <span translate>action.add</span>
  224 + </button>
210 </div> 225 </div>
211 - </ng-template>  
212 - <div fxFlex fxLayout="row" fxLayoutAlign="start center">  
213 - <button [disabled]="isLoading$ | async"  
214 - type="button"  
215 - mat-raised-button color="primary"  
216 - [fxShow]="modelValue?.typeParameters &&  
217 - (modelValue?.typeParameters.maxDatasources == -1 || datasourcesFormArray().controls.length < modelValue?.typeParameters.maxDatasources)"  
218 - (click)="addDatasource()"  
219 - matTooltip="{{ 'widget-config.add-datasource' | translate }}"  
220 - matTooltipPosition="above">  
221 - <mat-icon>add</mat-icon>  
222 - <span translate>action.add</span>  
223 - </button>  
224 - </div>  
225 - </mat-expansion-panel>  
226 - <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc &&  
227 - modelValue?.isDataEnabled" [expanded]="true">  
228 - <mat-expansion-panel-header>  
229 - <mat-panel-title>  
230 - {{ 'widget-config.target-device' | translate }}  
231 - </mat-panel-title>  
232 - </mat-expansion-panel-header>  
233 - <div [formGroup]="targetDeviceSettings" style="padding: 0 5px;">  
234 - <tb-entity-alias-select fxFlex  
235 - [tbRequired]="!widgetEditMode"  
236 - [aliasController]="aliasController"  
237 - [allowedEntityTypes]="[entityTypes.DEVICE]"  
238 - [callbacks]="widgetConfigCallbacks"  
239 - formControlName="targetDeviceAliasId">  
240 - </tb-entity-alias-select>  
241 - </div>  
242 - </mat-expansion-panel>  
243 - <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.alarm &&  
244 - modelValue?.isDataEnabled" [expanded]="true">  
245 - <mat-expansion-panel-header>  
246 - <mat-panel-title>  
247 - {{ 'widget-config.alarm-source' | translate }}  
248 - </mat-panel-title>  
249 - </mat-expansion-panel-header>  
250 - <div [formGroup]="alarmSourceSettings" style="padding: 0 5px;">  
251 - <section fxFlex  
252 - fxLayout="column"  
253 - fxLayoutAlign="center"  
254 - fxLayout.gt-sm="row"  
255 - fxLayoutAlign.gt-sm="start center">  
256 - <mat-form-field class="tb-datasource-type">  
257 - <mat-select formControlName="type">  
258 - <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">  
259 - {{ datasourceTypesTranslations.get(datasourceType) | translate }}  
260 - </mat-option>  
261 - </mat-select>  
262 - </mat-form-field>  
263 - <section class="tb-datasource" [ngSwitch]="alarmSourceSettings.get('type').value">  
264 - <ng-template [ngSwitchCase]="datasourceType.entity">  
265 - <tb-entity-alias-select  
266 - [showLabel]="true"  
267 - [tbRequired]="alarmSourceSettings.get('type').value === datasourceType.entity"  
268 - [aliasController]="aliasController"  
269 - formControlName="entityAliasId"  
270 - [callbacks]="widgetConfigCallbacks">  
271 - </tb-entity-alias-select>  
272 - <tb-filter-select  
273 - [showLabel]="true"  
274 - [aliasController]="aliasController"  
275 - formControlName="filterId"  
276 - [callbacks]="widgetConfigCallbacks">  
277 - </tb-filter-select>  
278 - </ng-template> 226 + </mat-expansion-panel>
  227 + <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.rpc &&
  228 + modelValue?.isDataEnabled" [expanded]="true">
  229 + <mat-expansion-panel-header>
  230 + <mat-panel-title>
  231 + {{ 'widget-config.target-device' | translate }}
  232 + </mat-panel-title>
  233 + </mat-expansion-panel-header>
  234 + <div [formGroup]="targetDeviceSettings" style="padding: 0 5px;">
  235 + <tb-entity-alias-select fxFlex
  236 + [tbRequired]="!widgetEditMode"
  237 + [aliasController]="aliasController"
  238 + [allowedEntityTypes]="[entityTypes.DEVICE]"
  239 + [callbacks]="widgetConfigCallbacks"
  240 + formControlName="targetDeviceAliasId">
  241 + </tb-entity-alias-select>
  242 + </div>
  243 + </mat-expansion-panel>
  244 + <mat-expansion-panel class="tb-datasources" *ngIf="widgetType === widgetTypes.alarm &&
  245 + modelValue?.isDataEnabled" [expanded]="true">
  246 + <mat-expansion-panel-header>
  247 + <mat-panel-title>
  248 + {{ 'widget-config.alarm-source' | translate }}
  249 + </mat-panel-title>
  250 + </mat-expansion-panel-header>
  251 + <div [formGroup]="alarmSourceSettings" style="padding: 0 5px;">
  252 + <section fxFlex
  253 + fxLayout="column"
  254 + fxLayoutAlign="center"
  255 + fxLayout.gt-sm="row"
  256 + fxLayoutAlign.gt-sm="start center">
  257 + <mat-form-field class="tb-datasource-type">
  258 + <mat-select formControlName="type">
  259 + <mat-option *ngFor="let datasourceType of datasourceTypes" [value]="datasourceType">
  260 + {{ datasourceTypesTranslations.get(datasourceType) | translate }}
  261 + </mat-option>
  262 + </mat-select>
  263 + </mat-form-field>
  264 + <section class="tb-datasource" [ngSwitch]="alarmSourceSettings.get('type').value">
  265 + <ng-template [ngSwitchCase]="datasourceType.entity">
  266 + <tb-entity-alias-select
  267 + [showLabel]="true"
  268 + [tbRequired]="alarmSourceSettings.get('type').value === datasourceType.entity"
  269 + [aliasController]="aliasController"
  270 + formControlName="entityAliasId"
  271 + [callbacks]="widgetConfigCallbacks">
  272 + </tb-entity-alias-select>
  273 + <tb-filter-select
  274 + [showLabel]="true"
  275 + [aliasController]="aliasController"
  276 + formControlName="filterId"
  277 + [callbacks]="widgetConfigCallbacks">
  278 + </tb-filter-select>
  279 + </ng-template>
  280 + </section>
  281 + <tb-data-keys class="tb-data-keys" fxFlex
  282 + [widgetType]="widgetType"
  283 + [datasourceType]="alarmSourceSettings.get('type').value"
  284 + [aliasController]="aliasController"
  285 + [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"
  286 + [callbacks]="widgetConfigCallbacks"
  287 + [entityAliasId]="alarmSourceSettings.get('entityAliasId').value"
  288 + formControlName="dataKeys">
  289 + </tb-data-keys>
279 </section> 290 </section>
280 - <tb-data-keys class="tb-data-keys" fxFlex  
281 - [widgetType]="widgetType"  
282 - [datasourceType]="alarmSourceSettings.get('type').value"  
283 - [aliasController]="aliasController"  
284 - [datakeySettingsSchema]="modelValue?.dataKeySettingsSchema"  
285 - [callbacks]="widgetConfigCallbacks"  
286 - [entityAliasId]="alarmSourceSettings.get('entityAliasId').value"  
287 - formControlName="dataKeys">  
288 - </tb-data-keys>  
289 - </section>  
290 - </div>  
291 - </mat-expansion-panel> 291 + </div>
  292 + </mat-expansion-panel>
  293 + <mat-expansion-panel [formGroup]="widgetSettings">
  294 + <mat-expansion-panel-header>
  295 + <mat-panel-title translate>widget-config.data-settings</mat-panel-title>
  296 + </mat-expansion-panel-header>
  297 + <ng-template matExpansionPanelContent>
  298 + <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
  299 + fxLayoutGap="8px">
  300 + <mat-form-field fxFlex>
  301 + <mat-label translate>widget-config.units</mat-label>
  302 + <input matInput formControlName="units">
  303 + </mat-form-field>
  304 + <mat-form-field fxFlex>
  305 + <mat-label translate>widget-config.decimals</mat-label>
  306 + <input matInput formControlName="decimals" type="number" min="0" max="15" step="1">
  307 + </mat-form-field>
  308 + </div>
  309 + </ng-template>
  310 + </mat-expansion-panel>
  311 + </mat-accordion>
292 </div> 312 </div>
293 </mat-tab> 313 </mat-tab>
294 <mat-tab label="{{ 'widget-config.settings' | translate }}"> 314 <mat-tab label="{{ 'widget-config.settings' | translate }}">
295 - <div class="mat-content mat-padding" fxLayout="column" fxLayoutGap="8px">  
296 - <div [formGroup]="widgetSettings" fxLayout="column" fxLayoutGap="8px">  
297 - <span translate>widget-config.general-settings</span>  
298 - <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center">  
299 - <div fxLayout="column" fxLayoutAlign="center" fxFlex.sm="40%" fxFlex.gt-sm="30%">  
300 - <mat-form-field fxFlex class="mat-block"> 315 + <div class="mat-content mat-padding" fxLayout="column">
  316 + <div [formGroup]="widgetSettings" fxLayout="column">
  317 + <fieldset class="fields-group" fxLayout="column">
  318 + <legend class="group-title" translate>widget-config.title</legend>
  319 + <mat-slide-toggle formControlName="showTitle" style="margin: 8px 0">
  320 + {{ 'widget-config.display-title' | translate }}
  321 + </mat-slide-toggle>
  322 + <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
  323 + fxLayoutGap="8px">
  324 + <mat-form-field fxFlex>
301 <mat-label translate>widget-config.title</mat-label> 325 <mat-label translate>widget-config.title</mat-label>
302 <input matInput formControlName="title"> 326 <input matInput formControlName="title">
303 </mat-form-field> 327 </mat-form-field>
@@ -306,130 +330,143 @@ @@ -306,130 +330,143 @@
306 <input matInput formControlName="titleTooltip"> 330 <input matInput formControlName="titleTooltip">
307 </mat-form-field> 331 </mat-form-field>
308 </div> 332 </div>
309 - <div fxFlex [fxShow]="widgetSettings.get('showTitle').value">  
310 - <tb-json-object-edit  
311 - [editorStyle]="{minHeight: '100px'}"  
312 - required  
313 - label="{{ 'widget-config.title-style' | translate }}"  
314 - formControlName="titleStyle"  
315 - ></tb-json-object-edit>  
316 - </div>  
317 - </div>  
318 - <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-md="row" fxLayoutAlign.gt-md="start center" fxFlex="100%"  
319 - fxLayoutGap="8px">  
320 - <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"  
321 - fxLayoutGap="8px" fxFlex.gt-md>  
322 - <mat-checkbox fxFlex formControlName="showTitleIcon"> 333 + <fieldset class="fields-group" fxLayout="column" fxLayoutGap="8px" style="margin: 0">
  334 + <legend class="group-title" translate>widget-config.title-icon</legend>
  335 + <mat-slide-toggle formControlName="showTitleIcon">
323 {{ 'widget-config.display-icon' | translate }} 336 {{ 'widget-config.display-icon' | translate }}
324 - </mat-checkbox>  
325 - <tb-material-icon-select fxFlex  
326 - formControlName="titleIcon">  
327 - </tb-material-icon-select>  
328 - </div>  
329 - <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"  
330 - fxLayoutGap="8px" fxFlex.gt-md>  
331 - <tb-color-input fxFlex  
332 - label="{{'widget-config.icon-color' | translate}}"  
333 - icon="format_color_fill"  
334 - openOnInput  
335 - formControlName="iconColor">  
336 - </tb-color-input>  
337 - <mat-form-field fxFlex>  
338 - <mat-label translate>widget-config.icon-size</mat-label>  
339 - <input matInput formControlName="iconSize">  
340 - </mat-form-field>  
341 - </div>  
342 - </div>  
343 - <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"  
344 - fxLayoutGap="8px">  
345 - <div fxLayout="column" fxLayoutAlign="center" fxLayoutGap="8px" fxFlex.sm="40%" fxFlex.gt-sm="30%">  
346 - <mat-checkbox formControlName="showTitle">  
347 - {{ 'widget-config.display-title' | translate }}  
348 - </mat-checkbox>  
349 - <mat-checkbox formControlName="dropShadow">  
350 - {{ 'widget-config.drop-shadow' | translate }}  
351 - </mat-checkbox>  
352 - <mat-checkbox formControlName="enableFullscreen">  
353 - {{ 'widget-config.enable-fullscreen' | translate }}  
354 - </mat-checkbox>  
355 - </div>  
356 - <div fxFlex>  
357 - <tb-json-object-edit  
358 - [editorStyle]="{minHeight: '100px'}"  
359 - required  
360 - label="{{ 'widget-config.widget-style' | translate }}"  
361 - formControlName="widgetStyle"  
362 - ></tb-json-object-edit>  
363 - </div>  
364 - </div>  
365 - <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-md="row" fxLayoutAlign.gt-md="start center"  
366 - fxFlex="100%" fxLayoutGap="8px">  
367 - <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"  
368 - fxLayoutGap="8px" fxFlex.gt-md>  
369 - <tb-color-input fxFlex  
370 - label="{{'widget-config.background-color' | translate}}"  
371 - icon="format_color_fill"  
372 - openOnInput  
373 - formControlName="backgroundColor">  
374 - </tb-color-input>  
375 - <tb-color-input fxFlex  
376 - label="{{'widget-config.text-color' | translate}}"  
377 - icon="format_color_fill"  
378 - openOnInput  
379 - formControlName="color">  
380 - </tb-color-input>  
381 - </div>  
382 - <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"  
383 - fxLayoutGap="8px" fxFlex.gt-md>  
384 - <mat-form-field fxFlex>  
385 - <mat-label translate>widget-config.padding</mat-label>  
386 - <input matInput formControlName="padding">  
387 - </mat-form-field>  
388 - <mat-form-field fxFlex>  
389 - <mat-label translate>widget-config.margin</mat-label>  
390 - <input matInput formControlName="margin">  
391 - </mat-form-field> 337 + </mat-slide-toggle>
  338 + <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row wrap" fxLayoutAlign="start center"
  339 + fxLayoutGap="8px">
  340 + <tb-material-icon-select fxFlex
  341 + formControlName="titleIcon">
  342 + </tb-material-icon-select>
  343 + <tb-color-input fxFlex
  344 + label="{{'widget-config.icon-color' | translate}}"
  345 + icon="format_color_fill"
  346 + openOnInput
  347 + formControlName="iconColor">
  348 + </tb-color-input>
  349 + <mat-form-field fxFlex>
  350 + <mat-label translate>widget-config.icon-size</mat-label>
  351 + <input matInput formControlName="iconSize">
  352 + </mat-form-field>
  353 + </div>
  354 + </fieldset>
  355 + <mat-expansion-panel class="tb-settings">
  356 + <mat-expansion-panel-header>
  357 + <mat-panel-description fxLayoutAlign="end" translate>
  358 + widget-config.advanced-settings
  359 + </mat-panel-description>
  360 + </mat-expansion-panel-header>
  361 + <ng-template matExpansionPanelContent>
  362 + <tb-json-object-edit
  363 + [editorStyle]="{minHeight: '100px'}"
  364 + required
  365 + label="{{ 'widget-config.title-style' | translate }}"
  366 + formControlName="titleStyle"
  367 + ></tb-json-object-edit>
  368 + </ng-template>
  369 + </mat-expansion-panel>
  370 + </fieldset>
  371 + <fieldset class="fields-group" fxLayout="column">
  372 + <legend class="group-title" translate>widget-config.widget-style</legend>
  373 + <div fxLayout="column" fxLayoutAlign="center" fxLayout.gt-md="row" fxLayoutAlign.gt-md="start center"
  374 + fxFlex="100%" fxLayoutGap="8px" class="tb-widget-style">
  375 + <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
  376 + fxLayoutGap="8px" fxFlex.gt-md>
  377 + <tb-color-input fxFlex
  378 + label="{{'widget-config.background-color' | translate}}"
  379 + icon="format_color_fill"
  380 + openOnInput
  381 + formControlName="backgroundColor">
  382 + </tb-color-input>
  383 + <tb-color-input fxFlex
  384 + label="{{'widget-config.text-color' | translate}}"
  385 + icon="format_color_fill"
  386 + openOnInput
  387 + formControlName="color">
  388 + </tb-color-input>
  389 + </div>
  390 + <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
  391 + fxLayoutGap="8px" fxFlex.gt-md>
  392 + <mat-form-field fxFlex>
  393 + <mat-label translate>widget-config.padding</mat-label>
  394 + <input matInput formControlName="padding">
  395 + </mat-form-field>
  396 + <mat-form-field fxFlex>
  397 + <mat-label translate>widget-config.margin</mat-label>
  398 + <input matInput formControlName="margin">
  399 + </mat-form-field>
  400 + </div>
392 </div> 401 </div>
393 - </div>  
394 - <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"  
395 - fxLayoutGap="8px">  
396 - <mat-form-field fxFlex>  
397 - <mat-label translate>widget-config.units</mat-label>  
398 - <input matInput formControlName="units">  
399 - </mat-form-field>  
400 - <mat-form-field fxFlex>  
401 - <mat-label translate>widget-config.decimals</mat-label>  
402 - <input matInput formControlName="decimals" type="number" min="0" max="15" step="1">  
403 - </mat-form-field>  
404 - </div>  
405 - <div [fxShow]="widgetType === widgetTypes.timeseries || widgetType === widgetTypes.latest"  
406 - fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"  
407 - fxLayoutGap="8px" fxFlex="100%">  
408 - <mat-checkbox fxFlex.gt-xs formControlName="showLegend">  
409 - {{ 'widget-config.display-legend' | translate }}  
410 - </mat-checkbox>  
411 - <section fxFlex.gt-xs fxLayout="row" fxLayoutAlign="start center" style="margin-bottom: 16px;">  
412 - <tb-legend-config formControlName="legendConfig">  
413 - </tb-legend-config>  
414 - </section>  
415 - </div>  
416 - </div>  
417 - <div [formGroup]="layoutSettings" fxLayout="column" fxLayoutGap="8px">  
418 - <span translate>widget-config.mobile-mode-settings</span>  
419 - <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"  
420 - fxLayoutGap="8px">  
421 - <mat-checkbox formControlName="mobileHide">  
422 - {{ 'widget-config.mobile-hide' | translate }}  
423 - </mat-checkbox>  
424 - <mat-form-field fxFlex>  
425 - <mat-label translate>widget-config.order</mat-label>  
426 - <input matInput formControlName="mobileOrder" type="number" step="1">  
427 - </mat-form-field>  
428 - <mat-form-field fxFlex>  
429 - <mat-label translate>widget-config.height</mat-label>  
430 - <input matInput formControlName="mobileHeight" type="number" min="1" max="10" step="1">  
431 - </mat-form-field>  
432 - </div> 402 + <mat-slide-toggle formControlName="dropShadow" style="margin-bottom: 8px">
  403 + {{ 'widget-config.drop-shadow' | translate }}
  404 + </mat-slide-toggle>
  405 + <mat-slide-toggle formControlName="enableFullscreen">
  406 + {{ 'widget-config.enable-fullscreen' | translate }}
  407 + </mat-slide-toggle>
  408 + <mat-expansion-panel class="tb-settings">
  409 + <mat-expansion-panel-header>
  410 + <mat-panel-description fxLayoutAlign="end" translate>
  411 + widget-config.advanced-settings
  412 + </mat-panel-description>
  413 + </mat-expansion-panel-header>
  414 + <ng-template matExpansionPanelContent>
  415 + <tb-json-object-edit
  416 + [editorStyle]="{minHeight: '100px'}"
  417 + required
  418 + label="{{ 'widget-config.widget-style' | translate }}"
  419 + formControlName="widgetStyle"
  420 + ></tb-json-object-edit>
  421 + </ng-template>
  422 + </mat-expansion-panel>
  423 + </fieldset>
  424 + <fieldset class="fields-group fields-group-slider" fxLayout="column">
  425 + <legend class="group-title" translate>widget-config.legend</legend>
  426 + <mat-expansion-panel class="tb-settings">
  427 + <mat-expansion-panel-header fxLayout="row wrap">
  428 + <mat-panel-title>
  429 + <mat-slide-toggle formControlName="showLegend" (click)="$event.stopPropagation()" fxLayoutAlign="center">
  430 + {{ 'widget-config.display-legend' | translate }}
  431 + </mat-slide-toggle>
  432 + </mat-panel-title>
  433 + <mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
  434 + widget-config.advanced-settings
  435 + </mat-panel-description>
  436 + </mat-expansion-panel-header>
  437 + <ng-template matExpansionPanelContent>
  438 + <tb-legend-config formControlName="legendConfig"></tb-legend-config>
  439 + </ng-template>
  440 + </mat-expansion-panel>
  441 + </fieldset>
  442 + <fieldset [formGroup]="layoutSettings" class="fields-group fields-group-slider" fxLayout="column">
  443 + <legend class="group-title" translate>widget-config.mobile-mode-settings</legend>
  444 + <mat-expansion-panel class="tb-settings">
  445 + <mat-expansion-panel-header>
  446 + <mat-panel-title>
  447 + <mat-slide-toggle formControlName="mobileHide" (click)="$event.stopPropagation()" fxLayoutAlign="center">
  448 + {{ 'widget-config.mobile-hide' | translate }}
  449 + </mat-slide-toggle>
  450 + </mat-panel-title>
  451 + <mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
  452 + widget-config.advanced-settings
  453 + </mat-panel-description>
  454 + </mat-expansion-panel-header>
  455 + <ng-template matExpansionPanelContent>
  456 + <div fxLayout.xs="column" fxLayoutAlign.xs="center" fxLayout="row" fxLayoutAlign="start center"
  457 + fxLayoutGap="8px">
  458 + <mat-form-field fxFlex>
  459 + <mat-label translate>widget-config.order</mat-label>
  460 + <input matInput formControlName="mobileOrder" type="number" step="1">
  461 + </mat-form-field>
  462 + <mat-form-field fxFlex>
  463 + <mat-label translate>widget-config.height</mat-label>
  464 + <input matInput formControlName="mobileHeight" type="number" min="1" max="10" step="1">
  465 + </mat-form-field>
  466 + </div>
  467 + </ng-template>
  468 + </mat-expansion-panel>
  469 + </fieldset>
433 </div> 470 </div>
434 </div> 471 </div>
435 </mat-tab> 472 </mat-tab>
@@ -20,9 +20,6 @@ @@ -20,9 +20,6 @@
20 .tb-advanced-widget-config { 20 .tb-advanced-widget-config {
21 height: 100%; 21 height: 100%;
22 } 22 }
23 - .tb-advanced-widget-config {  
24 - height: 100%;  
25 - }  
26 .tb-datasources { 23 .tb-datasources {
27 24
28 .handle { 25 .handle {
@@ -69,6 +66,28 @@ @@ -69,6 +66,28 @@
69 padding-left: 8px; 66 padding-left: 8px;
70 } 67 }
71 } 68 }
  69 + .fields-group {
  70 + padding: 0 16px 8px;
  71 + margin-bottom: 10px;
  72 + border: 1px groove rgba(0, 0, 0, .25);
  73 + border-radius: 4px;
  74 + legend {
  75 + color: rgba(0, 0, 0, .7);
  76 + width: fit-content;
  77 + }
  78 + }
  79 + .fields-group-slider {
  80 + padding: 0;
  81 + legend {
  82 + margin-left: 16px;
  83 + }
  84 + .tb-settings {
  85 + padding: 0 16px 8px;
  86 + }
  87 + }
  88 + .tb-widget-style {
  89 + margin-top: 16px;
  90 + }
72 } 91 }
73 } 92 }
74 93
@@ -94,6 +113,36 @@ @@ -94,6 +113,36 @@
94 white-space: normal; 113 white-space: normal;
95 } 114 }
96 .mat-expansion-panel { 115 .mat-expansion-panel {
  116 + &.tb-settings {
  117 + box-shadow: none;
  118 + .mat-content {
  119 + overflow: visible;
  120 + }
  121 + .mat-expansion-panel-header {
  122 + padding: 0;
  123 + &:hover {
  124 + background: none;
  125 + }
  126 + .mat-expansion-indicator {
  127 + padding: 2px;
  128 + }
  129 + }
  130 + .mat-expansion-panel-header-description {
  131 + align-items: center;
  132 + }
  133 + .mat-expansion-panel-body{
  134 + padding: 0;
  135 + }
  136 + .tb-json-object-panel {
  137 + margin: 0 0 8px;
  138 + }
  139 + .mat-checkbox-layout {
  140 + margin: 5px 0;
  141 + }
  142 + .mat-checkbox-inner-container {
  143 + margin-right: 12px;
  144 + }
  145 + }
97 &.tb-datasources { 146 &.tb-datasources {
98 &.mat-expanded { 147 &.mat-expanded {
99 overflow: visible; 148 overflow: visible;
@@ -152,5 +201,8 @@ @@ -152,5 +201,8 @@
152 } 201 }
153 } 202 }
154 } 203 }
  204 + .mat-slide-toggle-content {
  205 + white-space: normal;
  206 + }
155 } 207 }
156 } 208 }
@@ -212,11 +212,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -212,11 +212,28 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
212 showLegend: [null, []], 212 showLegend: [null, []],
213 legendConfig: [null, []] 213 legendConfig: [null, []]
214 }); 214 });
  215 + this.widgetSettings.get('showTitle').valueChanges.subscribe((value: boolean) => {
  216 + if (value) {
  217 + this.widgetSettings.get('titleStyle').enable({emitEvent: false});
  218 + this.widgetSettings.get('titleTooltip').enable({emitEvent: false});
  219 + this.widgetSettings.get('showTitleIcon').enable({emitEvent: false});
  220 + } else {
  221 + this.widgetSettings.get('titleStyle').disable({emitEvent: false});
  222 + this.widgetSettings.get('titleTooltip').disable({emitEvent: false});
  223 + this.widgetSettings.get('showTitleIcon').patchValue(false);
  224 + this.widgetSettings.get('showTitleIcon').disable({emitEvent: false});
  225 + }
  226 + });
  227 +
215 this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => { 228 this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => {
216 if (value) { 229 if (value) {
217 this.widgetSettings.get('titleIcon').enable({emitEvent: false}); 230 this.widgetSettings.get('titleIcon').enable({emitEvent: false});
  231 + this.widgetSettings.get('iconColor').enable({emitEvent: false});
  232 + this.widgetSettings.get('iconSize').enable({emitEvent: false});
218 } else { 233 } else {
219 this.widgetSettings.get('titleIcon').disable({emitEvent: false}); 234 this.widgetSettings.get('titleIcon').disable({emitEvent: false});
  235 + this.widgetSettings.get('iconColor').disable({emitEvent: false});
  236 + this.widgetSettings.get('iconSize').disable({emitEvent: false});
220 } 237 }
221 }); 238 });
222 this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => { 239 this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => {
@@ -236,6 +253,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -236,6 +253,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
236 }); 253 });
237 } 254 }
238 255
  256 + ngOnDestroy(): void {
  257 + this.removeChangeSubscriptions();
  258 + }
  259 +
239 private removeChangeSubscriptions() { 260 private removeChangeSubscriptions() {
240 if (this.dataSettingsChangesSubscription) { 261 if (this.dataSettingsChangesSubscription) {
241 this.dataSettingsChangesSubscription.unsubscribe(); 262 this.dataSettingsChangesSubscription.unsubscribe();
@@ -376,7 +397,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -376,7 +397,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
376 iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)', 397 iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)',
377 iconSize: isDefined(config.iconSize) ? config.iconSize : '24px', 398 iconSize: isDefined(config.iconSize) ? config.iconSize : '24px',
378 titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '', 399 titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '',
379 - showTitle: config.showTitle, 400 + showTitle: isDefined(config.showTitle) ? config.showTitle : false,
380 dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true, 401 dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true,
381 enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true, 402 enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true,
382 backgroundColor: config.backgroundColor, 403 backgroundColor: config.backgroundColor,
@@ -396,11 +417,25 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont @@ -396,11 +417,25 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
396 }, 417 },
397 {emitEvent: false} 418 {emitEvent: false}
398 ); 419 );
  420 + const showTitle: boolean = this.widgetSettings.get('showTitle').value;
  421 + if (showTitle) {
  422 + this.widgetSettings.get('titleTooltip').enable({emitEvent: false});
  423 + this.widgetSettings.get('titleStyle').enable({emitEvent: false});
  424 + this.widgetSettings.get('showTitleIcon').enable({emitEvent: false});
  425 + } else {
  426 + this.widgetSettings.get('titleTooltip').disable({emitEvent: false});
  427 + this.widgetSettings.get('titleStyle').disable({emitEvent: false});
  428 + this.widgetSettings.get('showTitleIcon').disable({emitEvent: false});
  429 + }
399 const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value; 430 const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value;
400 if (showTitleIcon) { 431 if (showTitleIcon) {
401 this.widgetSettings.get('titleIcon').enable({emitEvent: false}); 432 this.widgetSettings.get('titleIcon').enable({emitEvent: false});
  433 + this.widgetSettings.get('iconColor').enable({emitEvent: false});
  434 + this.widgetSettings.get('iconSize').enable({emitEvent: false});
402 } else { 435 } else {
403 this.widgetSettings.get('titleIcon').disable({emitEvent: false}); 436 this.widgetSettings.get('titleIcon').disable({emitEvent: false});
  437 + this.widgetSettings.get('iconColor').disable({emitEvent: false});
  438 + this.widgetSettings.get('iconSize').disable({emitEvent: false});
404 } 439 }
405 const showLegend: boolean = this.widgetSettings.get('showLegend').value; 440 const showLegend: boolean = this.widgetSettings.get('showLegend').value;
406 if (showLegend) { 441 if (showLegend) {
@@ -279,6 +279,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI @@ -279,6 +279,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
279 this.widgetContext.servicesMap = ServicesMap; 279 this.widgetContext.servicesMap = ServicesMap;
280 this.widgetContext.isEdit = this.isEdit; 280 this.widgetContext.isEdit = this.isEdit;
281 this.widgetContext.isMobile = this.isMobile; 281 this.widgetContext.isMobile = this.isMobile;
  282 + this.widgetContext.toastTargetId = this.toastTargetId;
282 283
283 this.widgetContext.subscriptionApi = { 284 this.widgetContext.subscriptionApi = {
284 createSubscription: this.createSubscription.bind(this), 285 createSubscription: this.createSubscription.bind(this),
@@ -230,6 +230,7 @@ export class WidgetContext { @@ -230,6 +230,7 @@ export class WidgetContext {
230 $scope: IDynamicWidgetComponent; 230 $scope: IDynamicWidgetComponent;
231 isEdit: boolean; 231 isEdit: boolean;
232 isMobile: boolean; 232 isMobile: boolean;
  233 + toastTargetId: string;
233 234
234 widgetNamespace?: string; 235 widgetNamespace?: string;
235 subscriptionApi?: WidgetSubscriptionApi; 236 subscriptionApi?: WidgetSubscriptionApi;
@@ -21,6 +21,7 @@ import { DOCUMENT } from '@angular/common'; @@ -21,6 +21,7 @@ import { DOCUMENT } from '@angular/common';
21 import { WINDOW } from '@core/services/window.service'; 21 import { WINDOW } from '@core/services/window.service';
22 import { Tokenizer } from 'marked'; 22 import { Tokenizer } from 'marked';
23 import * as marked from 'marked'; 23 import * as marked from 'marked';
  24 +import { Clipboard } from '@angular/cdk/clipboard';
24 25
25 const copyCodeBlock = '{:copy-code}'; 26 const copyCodeBlock = '{:copy-code}';
26 const codeStyleRegex = '^{:code-style="(.*)"}\n'; 27 const codeStyleRegex = '^{:code-style="(.*)"}\n';
@@ -47,6 +48,7 @@ export class MarkedOptionsService extends MarkedOptions { @@ -47,6 +48,7 @@ export class MarkedOptionsService extends MarkedOptions {
47 private id = 1; 48 private id = 1;
48 49
49 constructor(private translate: TranslateService, 50 constructor(private translate: TranslateService,
  51 + private clipboardService: Clipboard,
50 @Inject(WINDOW) private readonly window: Window, 52 @Inject(WINDOW) private readonly window: Window,
51 @Inject(DOCUMENT) private readonly document: Document) { 53 @Inject(DOCUMENT) private readonly document: Document) {
52 super(); 54 super();
@@ -162,7 +164,7 @@ export class MarkedOptionsService extends MarkedOptions { @@ -162,7 +164,7 @@ export class MarkedOptionsService extends MarkedOptions {
162 const copyWrapper = $('#codeWrapper' + id); 164 const copyWrapper = $('#codeWrapper' + id);
163 if (copyWrapper.hasClass('noChars')) { 165 if (copyWrapper.hasClass('noChars')) {
164 const text = decodeURIComponent($('#copyCodeId' + id).text()); 166 const text = decodeURIComponent($('#copyCodeId' + id).text());
165 - this.window.navigator.clipboard.writeText(text).then(() => { 167 + if (this.clipboardService.copy(text)) {
166 import('tooltipster').then( 168 import('tooltipster').then(
167 () => { 169 () => {
168 if (!copyWrapper.hasClass('tooltipstered')) { 170 if (!copyWrapper.hasClass('tooltipstered')) {
@@ -186,9 +188,8 @@ export class MarkedOptionsService extends MarkedOptions { @@ -186,9 +188,8 @@ export class MarkedOptionsService extends MarkedOptions {
186 } 188 }
187 const tooltip = copyWrapper.tooltipster('instance'); 189 const tooltip = copyWrapper.tooltipster('instance');
188 tooltip.open(); 190 tooltip.open();
189 - }  
190 - );  
191 - }); 191 + });
  192 + }
192 } 193 }
193 } 194 }
194 } 195 }
@@ -92,14 +92,16 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @@ -92,14 +92,16 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
92 } 92 }
93 93
94 openIconDialog() { 94 openIconDialog() {
95 - this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe(  
96 - (icon) => {  
97 - if (icon) {  
98 - this.materialIconFormGroup.patchValue(  
99 - {icon}, {emitEvent: true}  
100 - ); 95 + if (!this.disabled) {
  96 + this.dialogs.materialIconPicker(this.materialIconFormGroup.get('icon').value).subscribe(
  97 + (icon) => {
  98 + if (icon) {
  99 + this.materialIconFormGroup.patchValue(
  100 + {icon}, {emitEvent: true}
  101 + );
  102 + }
101 } 103 }
102 - }  
103 - ); 104 + );
  105 + }
104 } 106 }
105 } 107 }
  1 +<!--
  2 +
  3 + Copyright © 2016-2021 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div style="background: #fff;" [ngClass]="{'fill-height': fillHeight}"
  19 + tb-fullscreen
  20 + [fullscreen]="fullscreen" (fullscreenChanged)="onFullscreen()" fxLayout="column">
  21 + <div fxLayout="row" fxLayoutAlign="start center" style="height: 40px;" class="tb-protobuf-content-toolbar">
  22 + <label class="tb-title no-padding">{{ label }}</label>
  23 + <span fxFlex></span>
  24 + <button type="button"
  25 + mat-button *ngIf="!readonly && !disabled" class="tidy" (click)="beautifyProtobuf()">
  26 + {{'js-func.tidy' | translate }}
  27 + </button>
  28 + <fieldset style="width: initial">
  29 + <div matTooltip="{{(fullscreen ? 'fullscreen.exit' : 'fullscreen.expand') | translate}}"
  30 + matTooltipPosition="above"
  31 + style="border-radius: 50%"
  32 + (click)="fullscreen = !fullscreen">
  33 + <button type='button' mat-button mat-icon-button class="tb-mat-32">
  34 + <mat-icon class="material-icons">{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
  35 + </button>
  36 + </div>
  37 + </fieldset>
  38 + </div>
  39 + <div id="tb-protobuf-panel" tb-toast toastTarget="{{toastTargetId}}"
  40 + class="tb-protobuf-content-panel" fxLayout="column">
  41 + <div #protobufEditor id="tb-protobuf-input" [ngStyle]="editorStyle" [ngClass]="{'fill-height': fillHeight}"></div>
  42 + </div>
  43 +</div>
ui-ngx/src/app/shared/components/protobuf-content.component.scss renamed from ui-ngx/src/app/modules/home/components/widget/legend-config-panel.component.scss
@@ -14,19 +14,44 @@ @@ -14,19 +14,44 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 :host { 16 :host {
17 - width: 100%;  
18 - height: 100%;  
19 - form,  
20 - fieldset { 17 + position: relative;
  18 +
  19 + .fill-height {
21 height: 100%; 20 height: 100%;
22 } 21 }
  22 +}
23 23
24 - .mat-content {  
25 - overflow: hidden;  
26 - background-color: #fff; 24 +.tb-protobuf-content-toolbar {
  25 + button.mat-button, button.mat-icon-button, button.mat-icon-button.tb-mat-32 {
  26 + align-items: center;
  27 + vertical-align: middle;
  28 + min-width: 32px;
  29 + min-height: 15px;
  30 + padding: 4px;
  31 + margin: 0;
  32 + font-size: .8rem;
  33 + line-height: 15px;
  34 + color: #7b7b7b;
  35 + background: rgba(220, 220, 220, .35);
  36 + &:not(:last-child) {
  37 + margin-right: 4px;
  38 + }
27 } 39 }
  40 +}
  41 +
  42 +.tb-protobuf-content-panel {
  43 + height: 100%;
  44 + margin-left: 15px;
  45 + border: 1px solid #c0c0c0;
  46 +
  47 + #tb-protobuf-input {
  48 + width: 100%;
  49 + min-width: 200px;
  50 + min-height: 160px;
  51 + height: 100%;
28 52
29 - .mat-padding {  
30 - padding: 16px; 53 + &:not(.fill-height) {
  54 + min-height: 200px;
  55 + }
31 } 56 }
32 } 57 }
  1 +///
  2 +/// Copyright © 2016-2021 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import {
  18 + Component,
  19 + ElementRef,
  20 + forwardRef,
  21 + Input,
  22 + OnDestroy,
  23 + OnInit,
  24 + ViewChild
  25 +} from '@angular/core';
  26 +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
  27 +import { Ace } from 'ace-builds';
  28 +import { CancelAnimationFrame, RafService } from '@core/services/raf.service';
  29 +import { ResizeObserver } from '@juggle/resize-observer';
  30 +import { guid } from '@core/utils';
  31 +import { coerceBooleanProperty } from '@angular/cdk/coercion';
  32 +import { Store } from '@ngrx/store';
  33 +import { AppState } from '@core/core.state';
  34 +import { getAce } from '@shared/models/ace/ace.models';
  35 +import { beautifyJs } from '@shared/models/beautify.models';
  36 +
  37 +@Component({
  38 + selector: 'tb-protobuf-content',
  39 + templateUrl: './protobuf-content.component.html',
  40 + styleUrls: ['./protobuf-content.component.scss'],
  41 + providers: [
  42 + {
  43 + provide: NG_VALUE_ACCESSOR,
  44 + useExisting: forwardRef(() => ProtobufContentComponent),
  45 + multi: true
  46 + }
  47 + ]
  48 +})
  49 +export class ProtobufContentComponent implements OnInit, ControlValueAccessor, OnDestroy {
  50 +
  51 + @ViewChild('protobufEditor', {static: true})
  52 + protobufEditorElmRef: ElementRef;
  53 +
  54 + private protobufEditor: Ace.Editor;
  55 + private editorsResizeCaf: CancelAnimationFrame;
  56 + private editorResize$: ResizeObserver;
  57 + private ignoreChange = false;
  58 +
  59 + toastTargetId = `protobufContentEditor-${guid()}`;
  60 +
  61 + @Input() label: string;
  62 +
  63 + @Input() disabled: boolean;
  64 +
  65 + @Input() fillHeight: boolean;
  66 +
  67 + @Input() editorStyle: {[klass: string]: any};
  68 +
  69 + @Input() tbPlaceholder: string;
  70 +
  71 + private readonlyValue: boolean;
  72 + get readonly(): boolean {
  73 + return this.readonlyValue;
  74 + }
  75 + @Input()
  76 + set readonly(value: boolean) {
  77 + this.readonlyValue = coerceBooleanProperty(value);
  78 + }
  79 +
  80 + fullscreen = false;
  81 +
  82 + contentBody: string;
  83 +
  84 + errorShowed = false;
  85 +
  86 + private propagateChange = null;
  87 +
  88 + constructor(public elementRef: ElementRef,
  89 + protected store: Store<AppState>,
  90 + private raf: RafService) {
  91 + }
  92 +
  93 + ngOnInit(): void {
  94 + const editorElement = this.protobufEditorElmRef.nativeElement;
  95 + let editorOptions: Partial<Ace.EditorOptions> = {
  96 + mode: `ace/mode/protobuf`,
  97 + showGutter: true,
  98 + showPrintMargin: false,
  99 + readOnly: this.disabled || this.readonly,
  100 + };
  101 +
  102 + const advancedOptions = {
  103 + enableSnippets: true,
  104 + enableBasicAutocompletion: true,
  105 + enableLiveAutocompletion: true
  106 + };
  107 +
  108 + editorOptions = {...editorOptions, ...advancedOptions};
  109 + getAce().subscribe(
  110 + (ace) => {
  111 + this.protobufEditor = ace.edit(editorElement, editorOptions);
  112 + this.protobufEditor.session.setUseWrapMode(true);
  113 + this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
  114 + this.protobufEditor.setReadOnly(this.disabled || this.readonly);
  115 + this.protobufEditor.on('change', () => {
  116 + if (!this.ignoreChange) {
  117 + this.updateView();
  118 + }
  119 + });
  120 + this.editorResize$ = new ResizeObserver(() => {
  121 + this.onAceEditorResize();
  122 + });
  123 + this.editorResize$.observe(editorElement);
  124 + }
  125 + );
  126 + }
  127 +
  128 + ngOnDestroy(): void {
  129 + if (this.editorResize$) {
  130 + this.editorResize$.disconnect();
  131 + }
  132 + }
  133 +
  134 + registerOnChange(fn: any): void {
  135 + this.propagateChange = fn;
  136 + }
  137 +
  138 + registerOnTouched(fn: any): void {
  139 + }
  140 +
  141 + setDisabledState(isDisabled: boolean): void {
  142 + this.disabled = isDisabled;
  143 + if (this.protobufEditor) {
  144 + this.protobufEditor.setReadOnly(this.disabled || this.readonly);
  145 + }
  146 + }
  147 +
  148 + writeValue(value: string): void {
  149 + this.contentBody = value;
  150 + if (this.protobufEditor) {
  151 + this.ignoreChange = true;
  152 + this.protobufEditor.setValue(this.contentBody ? this.contentBody : '', -1);
  153 + this.ignoreChange = false;
  154 + }
  155 + }
  156 +
  157 + updateView() {
  158 + const editorValue = this.protobufEditor.getValue();
  159 + if (this.contentBody !== editorValue) {
  160 + this.contentBody = editorValue;
  161 + this.propagateChange(this.contentBody);
  162 + }
  163 + }
  164 +
  165 + beautifyProtobuf() {
  166 + beautifyJs(this.contentBody, {indent_size: 4, wrap_line_length: 60}).subscribe(
  167 + (res) => {
  168 + this.protobufEditor.setValue(res ? res : '', -1);
  169 + this.updateView();
  170 + }
  171 + );
  172 + }
  173 +
  174 + onFullscreen() {
  175 + if (this.protobufEditor) {
  176 + setTimeout(() => {
  177 + this.protobufEditor.resize();
  178 + }, 0);
  179 + }
  180 + }
  181 +
  182 + private onAceEditorResize() {
  183 + if (this.editorsResizeCaf) {
  184 + this.editorsResizeCaf();
  185 + this.editorsResizeCaf = null;
  186 + }
  187 + this.editorsResizeCaf = this.raf.raf(() => {
  188 + this.protobufEditor.resize();
  189 + this.protobufEditor.renderer.updateFull();
  190 + });
  191 + }
  192 +
  193 +}
@@ -36,6 +36,8 @@ export function loadAceDependencies(): Observable<any> { @@ -36,6 +36,8 @@ export function loadAceDependencies(): Observable<any> {
36 aceObservables.push(from(import('ace-builds/src-noconflict/mode-text'))); 36 aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
37 aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown'))); 37 aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
38 aceObservables.push(from(import('ace-builds/src-noconflict/mode-html'))); 38 aceObservables.push(from(import('ace-builds/src-noconflict/mode-html')));
  39 + aceObservables.push(from(import('ace-builds/src-noconflict/mode-c_cpp')));
  40 + aceObservables.push(from(import('ace-builds/src-noconflict/mode-protobuf')));
39 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java'))); 41 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
40 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css'))); 42 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css')));
41 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json'))); 43 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json')));
@@ -43,6 +45,8 @@ export function loadAceDependencies(): Observable<any> { @@ -43,6 +45,8 @@ export function loadAceDependencies(): Observable<any> {
43 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text'))); 45 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text')));
44 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown'))); 46 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown')));
45 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html'))); 47 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/html')));
  48 + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/c_cpp')));
  49 + aceObservables.push(from(import('ace-builds/src-noconflict/snippets/protobuf')));
46 aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate'))); 50 aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate')));
47 aceObservables.push(from(import('ace-builds/src-noconflict/theme-github'))); 51 aceObservables.push(from(import('ace-builds/src-noconflict/theme-github')));
48 return forkJoin(aceObservables).pipe( 52 return forkJoin(aceObservables).pipe(
@@ -155,6 +155,7 @@ import { MarkedOptionsService } from '@shared/components/marked-options.service' @@ -155,6 +155,7 @@ import { MarkedOptionsService } from '@shared/components/marked-options.service'
155 import { TbPopoverService } from '@shared/components/popover.service'; 155 import { TbPopoverService } from '@shared/components/popover.service';
156 import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens'; 156 import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
157 import { TbMarkdownComponent } from '@shared/components/markdown.component'; 157 import { TbMarkdownComponent } from '@shared/components/markdown.component';
  158 +import { ProtobufContentComponent } from './components/protobuf-content.component';
158 159
159 export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { 160 export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
160 return markedOptionsService; 161 return markedOptionsService;
@@ -268,7 +269,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @@ -268,7 +269,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
268 OtaPackageAutocompleteComponent, 269 OtaPackageAutocompleteComponent,
269 WidgetsBundleSearchComponent, 270 WidgetsBundleSearchComponent,
270 CopyButtonComponent, 271 CopyButtonComponent,
271 - TogglePasswordComponent 272 + TogglePasswordComponent,
  273 + ProtobufContentComponent
272 ], 274 ],
273 imports: [ 275 imports: [
274 CommonModule, 276 CommonModule,
@@ -458,7 +460,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) @@ -458,7 +460,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
458 OtaPackageAutocompleteComponent, 460 OtaPackageAutocompleteComponent,
459 WidgetsBundleSearchComponent, 461 WidgetsBundleSearchComponent,
460 CopyButtonComponent, 462 CopyButtonComponent,
461 - TogglePasswordComponent 463 + TogglePasswordComponent,
  464 + ProtobufContentComponent
462 ] 465 ]
463 }) 466 })
464 export class SharedModule { } 467 export class SharedModule { }
@@ -596,8 +596,8 @@ @@ -596,8 +596,8 @@
596 "make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε το dashboard '{{dashboardTitle}}' ιδιωτικό", 596 "make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε το dashboard '{{dashboardTitle}}' ιδιωτικό",
597 "make-private-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα γίνουν ιδιωτικά και δεν θα είναι διαθέσιμα από τρίτους.", 597 "make-private-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα γίνουν ιδιωτικά και δεν θα είναι διαθέσιμα από τρίτους.",
598 "make-private-dashboard": "Κάνε το dashboard ιδιωτικό", 598 "make-private-dashboard": "Κάνε το dashboard ιδιωτικό",
599 - "socialshare-text": "'{{dashboardTitle}}' powered by EyeTech",  
600 - "socialshare-title": "'{{dashboardTitle}}' powered by EyeTech", 599 + "socialshare-text": "'{{dashboardTitle}}' powered by ThingsBoard",
  600 + "socialshare-title": "'{{dashboardTitle}}' powered by ThingsBoard",
601 "select-dashboard": "Επιλογή dashboard", 601 "select-dashboard": "Επιλογή dashboard",
602 "no-dashboards-matching": "Δεν βρέθηκαν dashboards που να αντιστοιχούν σε '{{entity}}'.", 602 "no-dashboards-matching": "Δεν βρέθηκαν dashboards που να αντιστοιχούν σε '{{entity}}'.",
603 "dashboard-required": "Απαιτείται dashboard.", 603 "dashboard-required": "Απαιτείται dashboard.",
@@ -2496,7 +2496,7 @@ @@ -2496,7 +2496,7 @@
2496 "domain-name": "Όνομα Domain", 2496 "domain-name": "Όνομα Domain",
2497 "help-link-base-url": "Base url για συνδέσμους βοηθείας", 2497 "help-link-base-url": "Base url για συνδέσμους βοηθείας",
2498 "enable-help-links": "Ενεργοποίηση συνδέσμων βοηθείας", 2498 "enable-help-links": "Ενεργοποίηση συνδέσμων βοηθείας",
2499 - "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: gprs.cloud", 2499 + "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: thingsboard.io",
2500 "show-platform-name-version": "Εμφάνιση ονόματος και έκδοσης πλατφόρμας", 2500 "show-platform-name-version": "Εμφάνιση ονόματος και έκδοσης πλατφόρμας",
2501 "platform-name": "Όνομα πλατφόρμας", 2501 "platform-name": "Όνομα πλατφόρμας",
2502 "platform-version": "Έκδοση πλατφόρμας", 2502 "platform-version": "Έκδοση πλατφόρμας",
@@ -3030,7 +3030,7 @@ @@ -3030,7 +3030,7 @@
3030 "title": "Title", 3030 "title": "Title",
3031 "title-tooltip": "Title Tooltip", 3031 "title-tooltip": "Title Tooltip",
3032 "general-settings": "General settings", 3032 "general-settings": "General settings",
3033 - "display-title": "Display title", 3033 + "display-title": "Display widget title",
3034 "drop-shadow": "Drop shadow", 3034 "drop-shadow": "Drop shadow",
3035 "enable-fullscreen": "Enable fullscreen", 3035 "enable-fullscreen": "Enable fullscreen",
3036 "background-color": "Background color", 3036 "background-color": "Background color",
@@ -3039,7 +3039,7 @@ @@ -3039,7 +3039,7 @@
3039 "margin": "Margin", 3039 "margin": "Margin",
3040 "widget-style": "Widget style", 3040 "widget-style": "Widget style",
3041 "title-style": "Title style", 3041 "title-style": "Title style",
3042 - "mobile-mode-settings": "Mobile mode settings", 3042 + "mobile-mode-settings": "Mobile mode",
3043 "order": "Order", 3043 "order": "Order",
3044 "height": "Height", 3044 "height": "Height",
3045 "mobile-hide": "Hide widget in mobile mode", 3045 "mobile-hide": "Hide widget in mobile mode",
@@ -3048,6 +3048,7 @@ @@ -3048,6 +3048,7 @@
3048 "timewindow": "Timewindow", 3048 "timewindow": "Timewindow",
3049 "use-dashboard-timewindow": "Use dashboard timewindow", 3049 "use-dashboard-timewindow": "Use dashboard timewindow",
3050 "display-timewindow": "Display timewindow", 3050 "display-timewindow": "Display timewindow",
  3051 + "legend": "Legend",
3051 "display-legend": "Display legend", 3052 "display-legend": "Display legend",
3052 "datasources": "Datasources", 3053 "datasources": "Datasources",
3053 "maximum-datasources": "Maximum { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }", 3054 "maximum-datasources": "Maximum { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }",
@@ -3075,9 +3076,12 @@ @@ -3075,9 +3076,12 @@
3075 "delete-action": "Delete action", 3076 "delete-action": "Delete action",
3076 "delete-action-title": "Delete widget action", 3077 "delete-action-title": "Delete widget action",
3077 "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?", 3078 "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?",
  3079 + "title-icon": "Title icon",
3078 "display-icon": "Display title icon", 3080 "display-icon": "Display title icon",
3079 "icon-color": "Icon color", 3081 "icon-color": "Icon color",
3080 - "icon-size": "Icon size" 3082 + "icon-size": "Icon size",
  3083 + "advanced-settings": "Advanced settings",
  3084 + "data-settings": "Data settings"
3081 }, 3085 },
3082 "widget-type": { 3086 "widget-type": {
3083 "import": "Import widget type", 3087 "import": "Import widget type",
@@ -1470,7 +1470,7 @@ @@ -1470,7 +1470,7 @@
1470 "rulechain-required": "Chaîne de règles requise", 1470 "rulechain-required": "Chaîne de règles requise",
1471 "rulechains": "Chaînes de règles", 1471 "rulechains": "Chaînes de règles",
1472 "select-rulechain": "Sélectionner la chaîne de règles", 1472 "select-rulechain": "Sélectionner la chaîne de règles",
1473 - "set-root": "Rend la chaîne de règles racine (root) ", 1473 + "set-root": "Rendre la chaîne de règles racine (root) ",
1474 "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.", 1474 "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.",
1475 "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?", 1475 "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?",
1476 "system": "Système", 1476 "system": "Système",
@@ -1498,7 +1498,7 @@ @@ -1498,7 +1498,7 @@
1498 "edge-template-root": "Racine du modèle", 1498 "edge-template-root": "Racine du modèle",
1499 "search": "Rechercher des chaînes de règles", 1499 "search": "Rechercher des chaînes de règles",
1500 "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains} } sélectionné", 1500 "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains} } sélectionné",
1501 - "open-rulechain": "Chaîne de règles ouverte", 1501 + "open-rulechain": "Ouvrir la Chaîne de règles",
1502 "assign-to-edge": "Attribuer à Bordure", 1502 "assign-to-edge": "Attribuer à Bordure",
1503 "edge-rulechain": "Chaîne de règles Bordure", 1503 "edge-rulechain": "Chaîne de règles Bordure",
1504 "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains} }?" 1504 "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains} }?"
@@ -492,7 +492,7 @@ @@ -492,7 +492,7 @@
492 "make-private-dashboard-text": "დადასტურების შემდეგ დეშბორდი გახდება პრივატული და აღარ იქნება ხელმისაწვდომი სხვა მომხმარებლებისთვის", 492 "make-private-dashboard-text": "დადასტურების შემდეგ დეშბორდი გახდება პრივატული და აღარ იქნება ხელმისაწვდომი სხვა მომხმარებლებისთვის",
493 "make-private-dashboard": "აქციე დეშბორდი პრივატულად", 493 "make-private-dashboard": "აქციე დეშბორდი პრივატულად",
494 "socialshare-text": "სოციალური ტექსტი", 494 "socialshare-text": "სოციალური ტექსტი",
495 - "socialshare-title": "'{{dashboardTitle}}' Powered by Giot", 495 + "socialshare-title": "'{{dashboardTitle}}' Powered by ThingsBoard",
496 "select-dashboard": "აირჩიე დეშბორდი", 496 "select-dashboard": "აირჩიე დეშბორდი",
497 "no-dashboards-matching": "'{{entity}}'-ს მზგავსი დეშბორდი არ იქნა ნაპოვნი.", 497 "no-dashboards-matching": "'{{entity}}'-ს მზგავსი დეშბორდი არ იქნა ნაპოვნი.",
498 "dashboard-required": "დეშბორდი აუცილებელია", 498 "dashboard-required": "დეშბორდი აუცილებელია",
@@ -453,8 +453,8 @@ @@ -453,8 +453,8 @@
453 "make-private-dashboard-title": "Vai esat pārliecināts, ka vēlaties veidot paneli '{{dashboardTitle}}' privātu?", 453 "make-private-dashboard-title": "Vai esat pārliecināts, ka vēlaties veidot paneli '{{dashboardTitle}}' privātu?",
454 "make-private-dashboard-text": "Pēc apstiprinājuma panelis būs privāts un nebūs pieejams citiem.", 454 "make-private-dashboard-text": "Pēc apstiprinājuma panelis būs privāts un nebūs pieejams citiem.",
455 "make-private-dashboard": "Veidot paneli privātu", 455 "make-private-dashboard": "Veidot paneli privātu",
456 - "socialshare-text": "'{{dashboardTitle}}' atbalsts no TeT",  
457 - "socialshare-title": "'{{dashboardTitle}}' atbalsts no TeT", 456 + "socialshare-text": "'{{dashboardTitle}}' atbalsts no ThingsBoard",
  457 + "socialshare-title": "'{{dashboardTitle}}' atbalsts no ThingsBoard",
458 "select-dashboard": "Atlasīt paneli", 458 "select-dashboard": "Atlasīt paneli",
459 "no-dashboards-matching": "Nav atbilstoši paneļi '{{entity}}' atrasti.", 459 "no-dashboards-matching": "Nav atbilstoši paneļi '{{entity}}' atrasti.",
460 "dashboard-required": "Penelis ir nepieciešams.", 460 "dashboard-required": "Penelis ir nepieciešams.",
@@ -1631,7 +1631,7 @@ @@ -1631,7 +1631,7 @@
1631 "advanced": "Дополнительно", 1631 "advanced": "Дополнительно",
1632 "title": "Название", 1632 "title": "Название",
1633 "general-settings": "Общие настройки", 1633 "general-settings": "Общие настройки",
1634 - "display-title": "Показать название", 1634 + "display-title": "Показать название на виджете",
1635 "drop-shadow": "Тень", 1635 "drop-shadow": "Тень",
1636 "enable-fullscreen": "Во весь экран", 1636 "enable-fullscreen": "Во весь экран",
1637 "background-color": "Цвет фона", 1637 "background-color": "Цвет фона",
@@ -1640,7 +1640,7 @@ @@ -1640,7 +1640,7 @@
1640 "margin": "Margin", 1640 "margin": "Margin",
1641 "widget-style": "Стиль виджета", 1641 "widget-style": "Стиль виджета",
1642 "title-style": "Стиль названия", 1642 "title-style": "Стиль названия",
1643 - "mobile-mode-settings": "Настройки мобильного режима", 1643 + "mobile-mode-settings": "Мобильный режим",
1644 "order": "Порядок", 1644 "order": "Порядок",
1645 "height": "Высота", 1645 "height": "Высота",
1646 "units": "Специальный символ после значения", 1646 "units": "Специальный символ после значения",
@@ -1648,6 +1648,7 @@ @@ -1648,6 +1648,7 @@
1648 "timewindow": "Временное окно", 1648 "timewindow": "Временное окно",
1649 "use-dashboard-timewindow": "Использовать временное окно дашборда", 1649 "use-dashboard-timewindow": "Использовать временное окно дашборда",
1650 "display-timewindow": "Показывать временное окно", 1650 "display-timewindow": "Показывать временное окно",
  1651 + "legend": "Легенда",
1651 "display-legend": "Показать легенду", 1652 "display-legend": "Показать легенду",
1652 "datasources": "Источники данных", 1653 "datasources": "Источники данных",
1653 "maximum-datasources": "Максимальной количество источников данных равно {{count}}", 1654 "maximum-datasources": "Максимальной количество источников данных равно {{count}}",
@@ -1673,9 +1674,12 @@ @@ -1673,9 +1674,12 @@
1673 "delete-action": "Удалить действие", 1674 "delete-action": "Удалить действие",
1674 "delete-action-title": "Удалить действие виджета", 1675 "delete-action-title": "Удалить действие виджета",
1675 "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?", 1676 "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?",
1676 - "display-icon": "Показывать иконку в названии", 1677 + "title-icon": "Иконка в названии виджета",
  1678 + "display-icon": "Показывать иконку в названии виджета",
1677 "icon-color": "Цвет иконки", 1679 "icon-color": "Цвет иконки",
1678 - "icon-size": "Размер иконки" 1680 + "icon-size": "Размер иконки",
  1681 + "advanced-settings": "Расширенные настройки",
  1682 + "data-settings": "Настройки данных"
1679 }, 1683 },
1680 "widget-type": { 1684 "widget-type": {
1681 "import": "Импортировать тип виджета", 1685 "import": "Импортировать тип виджета",
@@ -2202,7 +2202,7 @@ @@ -2202,7 +2202,7 @@
2202 "advanced": "Додатково", 2202 "advanced": "Додатково",
2203 "title": "Назва", 2203 "title": "Назва",
2204 "general-settings": "Загальні налаштування", 2204 "general-settings": "Загальні налаштування",
2205 - "display-title": "Відобразити назву", 2205 + "display-title": "Відобразити назву у віджеті",
2206 "drop-shadow": "Тінь", 2206 "drop-shadow": "Тінь",
2207 "enable-fullscreen": "Увімкнути повноекранний режим", 2207 "enable-fullscreen": "Увімкнути повноекранний режим",
2208 "enable-data-export": "Увімкнути експорт даних", 2208 "enable-data-export": "Увімкнути експорт даних",
@@ -2212,7 +2212,7 @@ @@ -2212,7 +2212,7 @@
2212 "margin": "Границі", 2212 "margin": "Границі",
2213 "widget-style": "Стиль віджетів", 2213 "widget-style": "Стиль віджетів",
2214 "title-style": "Стиль заголовка", 2214 "title-style": "Стиль заголовка",
2215 - "mobile-mode-settings": "Налаштування мобільного режиму", 2215 + "mobile-mode-settings": "мобільний режим",
2216 "order": "Порядок", 2216 "order": "Порядок",
2217 "height": "Висота", 2217 "height": "Висота",
2218 "units": "Спеціальний символ після значення", 2218 "units": "Спеціальний символ після значення",
@@ -2220,6 +2220,7 @@ @@ -2220,6 +2220,7 @@
2220 "timewindow": "Вікно часу", 2220 "timewindow": "Вікно часу",
2221 "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації", 2221 "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації",
2222 "display-timewindow": "Показувати вікно часу", 2222 "display-timewindow": "Показувати вікно часу",
  2223 + "legend": "Легенда",
2223 "display-legend": "Показати легенду", 2224 "display-legend": "Показати легенду",
2224 "datasources": "Джерела даних", 2225 "datasources": "Джерела даних",
2225 "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }", 2226 "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }",
@@ -2245,9 +2246,12 @@ @@ -2245,9 +2246,12 @@
2245 "delete-action": "Видалити дію", 2246 "delete-action": "Видалити дію",
2246 "delete-action-title": "Видалити дію віджета", 2247 "delete-action-title": "Видалити дію віджета",
2247 "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?", 2248 "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?",
2248 - "display-icon": "Показувати іконку у назві", 2249 + "title-icon": "Іконка у назві віджету",
  2250 + "display-icon": "Показувати іконку у назві віджету",
2249 "icon-color": "Колір іконки", 2251 "icon-color": "Колір іконки",
2250 - "icon-size": "Розмір іконки" 2252 + "icon-size": "Розмір іконки",
  2253 + "advanced-settings": "Розширені налаштування",
  2254 + "data-settings": "Налаштування даних"
2251 }, 2255 },
2252 "widget-type": { 2256 "widget-type": {
2253 "import": "Імпортувати тип віджета", 2257 "import": "Імпортувати тип віджета",