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