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