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 182 HttpMethod method = HttpMethod.valueOf(config.getRequestMethod());
183 183 HttpEntity<String> entity;
184 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 187 entity = new HttpEntity<>(headers);
187 188 } else {
188 189 entity = new HttpEntity<>(msg.getData(), headers);
... ...
... ... @@ -47,6 +47,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
47 47 private String proxyPassword;
48 48 private String proxyScheme;
49 49 private ClientCredentials credentials;
  50 + private boolean ignoreRequestBody;
50 51
51 52 @Override
52 53 public TbRestApiCallNodeConfiguration defaultConfiguration() {
... ... @@ -61,6 +62,7 @@ public class TbRestApiCallNodeConfiguration implements NodeConfiguration<TbRestA
61 62 configuration.setTrimQueue(false);
62 63 configuration.setEnableProxy(false);
63 64 configuration.setCredentials(new AnonymousCredentials());
  65 + configuration.setIgnoreRequestBody(false);
64 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 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 19 fxFlex tb-fullscreen [fullscreen]="widgetEditMode || iframeMode || forceFullscreen || isFullscreen">
20 20 <tb-hotkeys-cheatsheet #cheatSheetComponent></tb-hotkeys-cheatsheet>
21 21 <section class="tb-dashboard-toolbar"
... ... @@ -139,6 +139,7 @@
139 139 </tb-dashboard-toolbar>
140 140 </section>
141 141 <section class="tb-dashboard-container tb-absolute-fill"
  142 + tb-toast toastTarget="dashboardRoot"
142 143 #dashboardContainer
143 144 [ngClass]="{ 'is-fullscreen': forceFullscreen,
144 145 'tb-dashboard-toolbar-opened': toolbarOpened,
... ...
... ... @@ -49,7 +49,6 @@ import { EntityAliasSelectComponent } from '@home/components/alias/entity-alias-
49 49 import { DataKeysComponent } from '@home/components/widget/data-keys.component';
50 50 import { DataKeyConfigDialogComponent } from '@home/components/widget/data-key-config-dialog.component';
51 51 import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component';
52   -import { LegendConfigPanelComponent } from '@home/components/widget/legend-config-panel.component';
53 52 import { LegendConfigComponent } from '@home/components/widget/legend-config.component';
54 53 import { ManageWidgetActionsComponent } from '@home/components/widget/action/manage-widget-actions.component';
55 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 181 DataKeysComponent,
183 182 DataKeyConfigComponent,
184 183 DataKeyConfigDialogComponent,
185   - LegendConfigPanelComponent,
186 184 LegendConfigComponent,
187 185 ManageWidgetActionsComponent,
188 186 WidgetActionDialogComponent,
... ...
... ... @@ -47,35 +47,50 @@
47 47 </mat-error>
48 48 </mat-form-field>
49 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 57 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
54 58 {{ 'device-profile.telemetry-proto-schema-required' | translate}}
55 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 68 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
61 69 {{ 'device-profile.attributes-proto-schema-required' | translate}}
62 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 79 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
68 80 {{ 'device-profile.rpc-request-proto-schema-required' | translate}}
69 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 90 <mat-error *ngIf="coapTransportConfigurationFormGroup.get('coapDeviceTypeConfiguration.transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
76 91 {{ 'device-profile.rpc-response-proto-schema-required' | translate}}
77 92 </mat-error>
78   - </mat-form-field>
  93 + </ng-container>
79 94 </div>
80 95 </div>
81 96 </fieldset>
... ...
... ... @@ -86,35 +86,50 @@
86 86 </div>
87 87 </div>
88 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 96 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceTelemetryProtoSchema').hasError('required')">
93 97 {{ 'device-profile.telemetry-proto-schema-required' | translate}}
94 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 107 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceAttributesProtoSchema').hasError('required')">
100 108 {{ 'device-profile.attributes-proto-schema-required' | translate}}
101 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 118 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcRequestProtoSchema').hasError('required')">
107 119 {{ 'device-profile.rpc-request-proto-schema-required' | translate}}
108 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 129 <mat-error *ngIf="mqttDeviceProfileTransportConfigurationFormGroup.get('transportPayloadTypeConfiguration.deviceRpcResponseProtoSchema').hasError('required')">
115 130 {{ 'device-profile.rpc-response-proto-schema-required' | translate}}
116 131 </mat-error>
117   - </mat-form-field>
  132 + </ng-container>
118 133 </div>
119 134 </div>
120 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 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 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 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 29 // @dynamic
45 30 @Component({
... ... @@ -58,105 +43,60 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
58 43
59 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 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 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 102 registerOnChange(fn: any): void {
... ... @@ -168,14 +108,29 @@ export class LegendConfigComponent implements OnInit, OnDestroy, ControlValueAcc
168 108
169 109 setDisabledState(isDisabled: boolean): void {
170 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 82 </mat-checkbox>
83 83 </div>
84 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 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 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 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 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 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 312 </div>
293 313 </mat-tab>
294 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 325 <mat-label translate>widget-config.title</mat-label>
302 326 <input matInput formControlName="title">
303 327 </mat-form-field>
... ... @@ -306,130 +330,143 @@
306 330 <input matInput formControlName="titleTooltip">
307 331 </mat-form-field>
308 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 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 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 470 </div>
434 471 </div>
435 472 </mat-tab>
... ...
... ... @@ -20,9 +20,6 @@
20 20 .tb-advanced-widget-config {
21 21 height: 100%;
22 22 }
23   - .tb-advanced-widget-config {
24   - height: 100%;
25   - }
26 23 .tb-datasources {
27 24
28 25 .handle {
... ... @@ -69,6 +66,28 @@
69 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 113 white-space: normal;
95 114 }
96 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 146 &.tb-datasources {
98 147 &.mat-expanded {
99 148 overflow: visible;
... ... @@ -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 212 showLegend: [null, []],
213 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 228 this.widgetSettings.get('showTitleIcon').valueChanges.subscribe((value: boolean) => {
216 229 if (value) {
217 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 233 } else {
219 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 239 this.widgetSettings.get('showLegend').valueChanges.subscribe((value: boolean) => {
... ... @@ -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 260 private removeChangeSubscriptions() {
240 261 if (this.dataSettingsChangesSubscription) {
241 262 this.dataSettingsChangesSubscription.unsubscribe();
... ... @@ -376,7 +397,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
376 397 iconColor: isDefined(config.iconColor) ? config.iconColor : 'rgba(0, 0, 0, 0.87)',
377 398 iconSize: isDefined(config.iconSize) ? config.iconSize : '24px',
378 399 titleTooltip: isDefined(config.titleTooltip) ? config.titleTooltip : '',
379   - showTitle: config.showTitle,
  400 + showTitle: isDefined(config.showTitle) ? config.showTitle : false,
380 401 dropShadow: isDefined(config.dropShadow) ? config.dropShadow : true,
381 402 enableFullscreen: isDefined(config.enableFullscreen) ? config.enableFullscreen : true,
382 403 backgroundColor: config.backgroundColor,
... ... @@ -396,11 +417,25 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont
396 417 },
397 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 430 const showTitleIcon: boolean = this.widgetSettings.get('showTitleIcon').value;
400 431 if (showTitleIcon) {
401 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 435 } else {
403 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 440 const showLegend: boolean = this.widgetSettings.get('showLegend').value;
406 441 if (showLegend) {
... ...
... ... @@ -279,6 +279,7 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI
279 279 this.widgetContext.servicesMap = ServicesMap;
280 280 this.widgetContext.isEdit = this.isEdit;
281 281 this.widgetContext.isMobile = this.isMobile;
  282 + this.widgetContext.toastTargetId = this.toastTargetId;
282 283
283 284 this.widgetContext.subscriptionApi = {
284 285 createSubscription: this.createSubscription.bind(this),
... ...
... ... @@ -230,6 +230,7 @@ export class WidgetContext {
230 230 $scope: IDynamicWidgetComponent;
231 231 isEdit: boolean;
232 232 isMobile: boolean;
  233 + toastTargetId: string;
233 234
234 235 widgetNamespace?: string;
235 236 subscriptionApi?: WidgetSubscriptionApi;
... ...
... ... @@ -21,6 +21,7 @@ import { DOCUMENT } from '@angular/common';
21 21 import { WINDOW } from '@core/services/window.service';
22 22 import { Tokenizer } from 'marked';
23 23 import * as marked from 'marked';
  24 +import { Clipboard } from '@angular/cdk/clipboard';
24 25
25 26 const copyCodeBlock = '{:copy-code}';
26 27 const codeStyleRegex = '^{:code-style="(.*)"}\n';
... ... @@ -47,6 +48,7 @@ export class MarkedOptionsService extends MarkedOptions {
47 48 private id = 1;
48 49
49 50 constructor(private translate: TranslateService,
  51 + private clipboardService: Clipboard,
50 52 @Inject(WINDOW) private readonly window: Window,
51 53 @Inject(DOCUMENT) private readonly document: Document) {
52 54 super();
... ... @@ -162,7 +164,7 @@ export class MarkedOptionsService extends MarkedOptions {
162 164 const copyWrapper = $('#codeWrapper' + id);
163 165 if (copyWrapper.hasClass('noChars')) {
164 166 const text = decodeURIComponent($('#copyCodeId' + id).text());
165   - this.window.navigator.clipboard.writeText(text).then(() => {
  167 + if (this.clipboardService.copy(text)) {
166 168 import('tooltipster').then(
167 169 () => {
168 170 if (!copyWrapper.hasClass('tooltipstered')) {
... ... @@ -186,9 +188,8 @@ export class MarkedOptionsService extends MarkedOptions {
186 188 }
187 189 const tooltip = copyWrapper.tooltipster('instance');
188 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 92 }
93 93
94 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 14 * limitations under the License.
15 15 */
16 16 :host {
17   - width: 100%;
18   - height: 100%;
19   - form,
20   - fieldset {
  17 + position: relative;
  18 +
  19 + .fill-height {
21 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 36 aceObservables.push(from(import('ace-builds/src-noconflict/mode-text')));
37 37 aceObservables.push(from(import('ace-builds/src-noconflict/mode-markdown')));
38 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 41 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/java')));
40 42 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/css')));
41 43 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/json')));
... ... @@ -43,6 +45,8 @@ export function loadAceDependencies(): Observable<any> {
43 45 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/text')));
44 46 aceObservables.push(from(import('ace-builds/src-noconflict/snippets/markdown')));
45 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 50 aceObservables.push(from(import('ace-builds/src-noconflict/theme-textmate')));
47 51 aceObservables.push(from(import('ace-builds/src-noconflict/theme-github')));
48 52 return forkJoin(aceObservables).pipe(
... ...
... ... @@ -155,6 +155,7 @@ import { MarkedOptionsService } from '@shared/components/marked-options.service'
155 155 import { TbPopoverService } from '@shared/components/popover.service';
156 156 import { HELP_MARKDOWN_COMPONENT_TOKEN, SHARED_MODULE_TOKEN } from '@shared/components/tokens';
157 157 import { TbMarkdownComponent } from '@shared/components/markdown.component';
  158 +import { ProtobufContentComponent } from './components/protobuf-content.component';
158 159
159 160 export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
160 161 return markedOptionsService;
... ... @@ -268,7 +269,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
268 269 OtaPackageAutocompleteComponent,
269 270 WidgetsBundleSearchComponent,
270 271 CopyButtonComponent,
271   - TogglePasswordComponent
  272 + TogglePasswordComponent,
  273 + ProtobufContentComponent
272 274 ],
273 275 imports: [
274 276 CommonModule,
... ... @@ -458,7 +460,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
458 460 OtaPackageAutocompleteComponent,
459 461 WidgetsBundleSearchComponent,
460 462 CopyButtonComponent,
461   - TogglePasswordComponent
  463 + TogglePasswordComponent,
  464 + ProtobufContentComponent
462 465 ]
463 466 })
464 467 export class SharedModule { }
... ...
... ... @@ -596,8 +596,8 @@
596 596 "make-private-dashboard-title": "Είστε σίγουροι ότι θέλετε να κάνετε το dashboard '{{dashboardTitle}}' ιδιωτικό",
597 597 "make-private-dashboard-text": "Μετά την επιβεβαίωση το dashboard θα γίνουν ιδιωτικά και δεν θα είναι διαθέσιμα από τρίτους.",
598 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 601 "select-dashboard": "Επιλογή dashboard",
602 602 "no-dashboards-matching": "Δεν βρέθηκαν dashboards που να αντιστοιχούν σε '{{entity}}'.",
603 603 "dashboard-required": "Απαιτείται dashboard.",
... ... @@ -2496,7 +2496,7 @@
2496 2496 "domain-name": "Όνομα Domain",
2497 2497 "help-link-base-url": "Base url για συνδέσμους βοηθείας",
2498 2498 "enable-help-links": "Ενεργοποίηση συνδέσμων βοηθείας",
2499   - "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: gprs.cloud",
  2499 + "error-verification-url": "Ένα όνομα domain δεν πρέπει να περιέχει σύμβολα '/' και ':'. Παράδειγμα: thingsboard.io",
2500 2500 "show-platform-name-version": "Εμφάνιση ονόματος και έκδοσης πλατφόρμας",
2501 2501 "platform-name": "Όνομα πλατφόρμας",
2502 2502 "platform-version": "Έκδοση πλατφόρμας",
... ...
... ... @@ -3030,7 +3030,7 @@
3030 3030 "title": "Title",
3031 3031 "title-tooltip": "Title Tooltip",
3032 3032 "general-settings": "General settings",
3033   - "display-title": "Display title",
  3033 + "display-title": "Display widget title",
3034 3034 "drop-shadow": "Drop shadow",
3035 3035 "enable-fullscreen": "Enable fullscreen",
3036 3036 "background-color": "Background color",
... ... @@ -3039,7 +3039,7 @@
3039 3039 "margin": "Margin",
3040 3040 "widget-style": "Widget style",
3041 3041 "title-style": "Title style",
3042   - "mobile-mode-settings": "Mobile mode settings",
  3042 + "mobile-mode-settings": "Mobile mode",
3043 3043 "order": "Order",
3044 3044 "height": "Height",
3045 3045 "mobile-hide": "Hide widget in mobile mode",
... ... @@ -3048,6 +3048,7 @@
3048 3048 "timewindow": "Timewindow",
3049 3049 "use-dashboard-timewindow": "Use dashboard timewindow",
3050 3050 "display-timewindow": "Display timewindow",
  3051 + "legend": "Legend",
3051 3052 "display-legend": "Display legend",
3052 3053 "datasources": "Datasources",
3053 3054 "maximum-datasources": "Maximum { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }",
... ... @@ -3075,9 +3076,12 @@
3075 3076 "delete-action": "Delete action",
3076 3077 "delete-action-title": "Delete widget action",
3077 3078 "delete-action-text": "Are you sure you want delete widget action with name '{{actionName}}'?",
  3079 + "title-icon": "Title icon",
3078 3080 "display-icon": "Display title icon",
3079 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 3086 "widget-type": {
3083 3087 "import": "Import widget type",
... ...
... ... @@ -1470,7 +1470,7 @@
1470 1470 "rulechain-required": "Chaîne de règles requise",
1471 1471 "rulechains": "Chaînes de règles",
1472 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 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 1475 "set-root-rulechain-title": "Voulez-vous vraiment que la chaîne de règles '{{ruleChainName}} soit racine (root) ?",
1476 1476 "system": "Système",
... ... @@ -1498,7 +1498,7 @@
1498 1498 "edge-template-root": "Racine du modèle",
1499 1499 "search": "Rechercher des chaînes de règles",
1500 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 1502 "assign-to-edge": "Attribuer à Bordure",
1503 1503 "edge-rulechain": "Chaîne de règles Bordure",
1504 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 492 "make-private-dashboard-text": "დადასტურების შემდეგ დეშბორდი გახდება პრივატული და აღარ იქნება ხელმისაწვდომი სხვა მომხმარებლებისთვის",
493 493 "make-private-dashboard": "აქციე დეშბორდი პრივატულად",
494 494 "socialshare-text": "სოციალური ტექსტი",
495   - "socialshare-title": "'{{dashboardTitle}}' Powered by Giot",
  495 + "socialshare-title": "'{{dashboardTitle}}' Powered by ThingsBoard",
496 496 "select-dashboard": "აირჩიე დეშბორდი",
497 497 "no-dashboards-matching": "'{{entity}}'-ს მზგავსი დეშბორდი არ იქნა ნაპოვნი.",
498 498 "dashboard-required": "დეშბორდი აუცილებელია",
... ...
... ... @@ -453,8 +453,8 @@
453 453 "make-private-dashboard-title": "Vai esat pārliecināts, ka vēlaties veidot paneli '{{dashboardTitle}}' privātu?",
454 454 "make-private-dashboard-text": "Pēc apstiprinājuma panelis būs privāts un nebūs pieejams citiem.",
455 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 458 "select-dashboard": "Atlasīt paneli",
459 459 "no-dashboards-matching": "Nav atbilstoši paneļi '{{entity}}' atrasti.",
460 460 "dashboard-required": "Penelis ir nepieciešams.",
... ...
... ... @@ -1631,7 +1631,7 @@
1631 1631 "advanced": "Дополнительно",
1632 1632 "title": "Название",
1633 1633 "general-settings": "Общие настройки",
1634   - "display-title": "Показать название",
  1634 + "display-title": "Показать название на виджете",
1635 1635 "drop-shadow": "Тень",
1636 1636 "enable-fullscreen": "Во весь экран",
1637 1637 "background-color": "Цвет фона",
... ... @@ -1640,7 +1640,7 @@
1640 1640 "margin": "Margin",
1641 1641 "widget-style": "Стиль виджета",
1642 1642 "title-style": "Стиль названия",
1643   - "mobile-mode-settings": "Настройки мобильного режима",
  1643 + "mobile-mode-settings": "Мобильный режим",
1644 1644 "order": "Порядок",
1645 1645 "height": "Высота",
1646 1646 "units": "Специальный символ после значения",
... ... @@ -1648,6 +1648,7 @@
1648 1648 "timewindow": "Временное окно",
1649 1649 "use-dashboard-timewindow": "Использовать временное окно дашборда",
1650 1650 "display-timewindow": "Показывать временное окно",
  1651 + "legend": "Легенда",
1651 1652 "display-legend": "Показать легенду",
1652 1653 "datasources": "Источники данных",
1653 1654 "maximum-datasources": "Максимальной количество источников данных равно {{count}}",
... ... @@ -1673,9 +1674,12 @@
1673 1674 "delete-action": "Удалить действие",
1674 1675 "delete-action-title": "Удалить действие виджета",
1675 1676 "delete-action-text": "Вы точно хотите удалить действие виджета '{{actionName}}'?",
1676   - "display-icon": "Показывать иконку в названии",
  1677 + "title-icon": "Иконка в названии виджета",
  1678 + "display-icon": "Показывать иконку в названии виджета",
1677 1679 "icon-color": "Цвет иконки",
1678   - "icon-size": "Размер иконки"
  1680 + "icon-size": "Размер иконки",
  1681 + "advanced-settings": "Расширенные настройки",
  1682 + "data-settings": "Настройки данных"
1679 1683 },
1680 1684 "widget-type": {
1681 1685 "import": "Импортировать тип виджета",
... ...
... ... @@ -2202,7 +2202,7 @@
2202 2202 "advanced": "Додатково",
2203 2203 "title": "Назва",
2204 2204 "general-settings": "Загальні налаштування",
2205   - "display-title": "Відобразити назву",
  2205 + "display-title": "Відобразити назву у віджеті",
2206 2206 "drop-shadow": "Тінь",
2207 2207 "enable-fullscreen": "Увімкнути повноекранний режим",
2208 2208 "enable-data-export": "Увімкнути експорт даних",
... ... @@ -2212,7 +2212,7 @@
2212 2212 "margin": "Границі",
2213 2213 "widget-style": "Стиль віджетів",
2214 2214 "title-style": "Стиль заголовка",
2215   - "mobile-mode-settings": "Налаштування мобільного режиму",
  2215 + "mobile-mode-settings": "мобільний режим",
2216 2216 "order": "Порядок",
2217 2217 "height": "Висота",
2218 2218 "units": "Спеціальний символ після значення",
... ... @@ -2220,6 +2220,7 @@
2220 2220 "timewindow": "Вікно часу",
2221 2221 "use-dashboard-timewindow": "Використати вікно часу на панелі візуалізації",
2222 2222 "display-timewindow": "Показувати вікно часу",
  2223 + "legend": "Легенда",
2223 2224 "display-legend": "Показати легенду",
2224 2225 "datasources": "Джерела даних",
2225 2226 "maximum-datasources": "Максимально { count, plural, 1 {1 дозволене джерело даних.} other {# дозволені джерела даних } }",
... ... @@ -2245,9 +2246,12 @@
2245 2246 "delete-action": "Видалити дію",
2246 2247 "delete-action-title": "Видалити дію віджета",
2247 2248 "delete-action-text": "Ви впевнені, що хочете видалити дію віджета '{{actionName}}'?",
2248   - "display-icon": "Показувати іконку у назві",
  2249 + "title-icon": "Іконка у назві віджету",
  2250 + "display-icon": "Показувати іконку у назві віджету",
2249 2251 "icon-color": "Колір іконки",
2250   - "icon-size": "Розмір іконки"
  2252 + "icon-size": "Розмір іконки",
  2253 + "advanced-settings": "Розширені налаштування",
  2254 + "data-settings": "Налаштування даних"
2251 2255 },
2252 2256 "widget-type": {
2253 2257 "import": "Імпортувати тип віджета",
... ...