Commit 22a1d24291d8e0a794f45935666f1c6fe3710676

Authored by Vladyslav_Prykhodko
1 parent 4c87b36a

Added to widgets bundles and widget functional preview and description

Showing 52 changed files with 398 additions and 159 deletions
  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 +ALTER TABLE widget_type
  18 + ADD COLUMN IF NOT EXISTS image varchar (1000000),
  19 + ADD COLUMN IF NOT EXISTS description varchar (255);
  20 +
  21 +ALTER TABLE widgets_bundle
  22 + ADD COLUMN IF NOT EXISTS image varchar (1000000),
  23 + ADD COLUMN IF NOT EXISTS description varchar (255);
@@ -187,6 +187,7 @@ public class ThingsboardInstallService { @@ -187,6 +187,7 @@ public class ThingsboardInstallService {
187 databaseEntitiesUpgradeService.upgradeDatabase("3.2.0"); 187 databaseEntitiesUpgradeService.upgradeDatabase("3.2.0");
188 case "3.2.1": 188 case "3.2.1":
189 log.info("Upgrading ThingsBoard from version 3.2.1 to 3.3.0 ..."); 189 log.info("Upgrading ThingsBoard from version 3.2.1 to 3.3.0 ...");
  190 + databaseEntitiesUpgradeService.upgradeDatabase("3.2.1");
190 log.info("Updating system data..."); 191 log.info("Updating system data...");
191 systemDataLoaderService.updateSystemWidgets(); 192 systemDataLoaderService.updateSystemWidgets();
192 break; 193 break;
@@ -434,6 +434,17 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService @@ -434,6 +434,17 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
434 log.info("Schema updated."); 434 log.info("Schema updated.");
435 } 435 }
436 break; 436 break;
  437 + case "3.2.1":
  438 + try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
  439 + log.info("Updating schema ...");
  440 + schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.2.1", SCHEMA_UPDATE_SQL);
  441 + loadSql(schemaUpdateFile, conn);
  442 + conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3003000;");
  443 + log.info("Schema updated.");
  444 + } catch (Exception e) {
  445 + log.error("Failed updating schema!!!", e);
  446 + }
  447 + break;
437 default: 448 default:
438 throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); 449 throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
439 } 450 }
@@ -31,6 +31,8 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId { @@ -31,6 +31,8 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
31 private String bundleAlias; 31 private String bundleAlias;
32 private String alias; 32 private String alias;
33 private String name; 33 private String name;
  34 + private String image;
  35 + private String description;
34 private transient JsonNode descriptor; 36 private transient JsonNode descriptor;
35 37
36 public WidgetType() { 38 public WidgetType() {
@@ -47,6 +49,8 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId { @@ -47,6 +49,8 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
47 this.bundleAlias = widgetType.getBundleAlias(); 49 this.bundleAlias = widgetType.getBundleAlias();
48 this.alias = widgetType.getAlias(); 50 this.alias = widgetType.getAlias();
49 this.name = widgetType.getName(); 51 this.name = widgetType.getName();
  52 + this.image = widgetType.getImage();
  53 + this.description = widgetType.getDescription();
50 this.descriptor = widgetType.getDescriptor(); 54 this.descriptor = widgetType.getDescriptor();
51 } 55 }
52 56
@@ -82,6 +86,14 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId { @@ -82,6 +86,14 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
82 this.name = name; 86 this.name = name;
83 } 87 }
84 88
  89 + public String getImage() { return image; }
  90 +
  91 + public void setImage(String image) { this.image = image; }
  92 +
  93 + public String getDescription() { return description; }
  94 +
  95 + public void setDescription(String description) { this.description = description; }
  96 +
85 public JsonNode getDescriptor() { 97 public JsonNode getDescriptor() {
86 return descriptor; 98 return descriptor;
87 } 99 }
@@ -97,9 +109,10 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId { @@ -97,9 +109,10 @@ public class WidgetType extends BaseData<WidgetTypeId> implements HasTenantId {
97 sb.append(", bundleAlias='").append(bundleAlias).append('\''); 109 sb.append(", bundleAlias='").append(bundleAlias).append('\'');
98 sb.append(", alias='").append(alias).append('\''); 110 sb.append(", alias='").append(alias).append('\'');
99 sb.append(", name='").append(name).append('\''); 111 sb.append(", name='").append(name).append('\'');
  112 + sb.append(", image='").append(image).append('\'');
  113 + sb.append(", description='").append(description).append('\'');
100 sb.append(", descriptor=").append(descriptor); 114 sb.append(", descriptor=").append(descriptor);
101 sb.append('}'); 115 sb.append('}');
102 return sb.toString(); 116 return sb.toString();
103 } 117 }
104 -  
105 } 118 }
@@ -29,7 +29,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H @@ -29,7 +29,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
29 private TenantId tenantId; 29 private TenantId tenantId;
30 private String alias; 30 private String alias;
31 private String title; 31 private String title;
32 - private byte[] image; 32 + private String image;
  33 + private String description;
33 34
34 public WidgetsBundle() { 35 public WidgetsBundle() {
35 super(); 36 super();
@@ -45,6 +46,7 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H @@ -45,6 +46,7 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
45 this.alias = widgetsBundle.getAlias(); 46 this.alias = widgetsBundle.getAlias();
46 this.title = widgetsBundle.getTitle(); 47 this.title = widgetsBundle.getTitle();
47 this.image = widgetsBundle.getImage(); 48 this.image = widgetsBundle.getImage();
  49 + this.description = widgetsBundle.getDescription();
48 } 50 }
49 51
50 public TenantId getTenantId() { 52 public TenantId getTenantId() {
@@ -71,14 +73,18 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H @@ -71,14 +73,18 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
71 this.title = title; 73 this.title = title;
72 } 74 }
73 75
74 - public byte[] getImage() { 76 + public String getImage() {
75 return image; 77 return image;
76 } 78 }
77 79
78 - public void setImage(byte[] image) { 80 + public void setImage(String image) {
79 this.image = image; 81 this.image = image;
80 } 82 }
81 83
  84 + public String getDescription() { return description; }
  85 +
  86 + public void setDescription(String description) { this.description = description; }
  87 +
82 @Override 88 @Override
83 public String getSearchText() { 89 public String getSearchText() {
84 return getTitle(); 90 return getTitle();
@@ -90,7 +96,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H @@ -90,7 +96,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
90 result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0); 96 result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
91 result = 31 * result + (alias != null ? alias.hashCode() : 0); 97 result = 31 * result + (alias != null ? alias.hashCode() : 0);
92 result = 31 * result + (title != null ? title.hashCode() : 0); 98 result = 31 * result + (title != null ? title.hashCode() : 0);
93 - result = 31 * result + Arrays.hashCode(image); 99 + result = 31 * result + (image != null ? image.hashCode() : 0);
  100 + result = 31 * result + (description != null ? description.hashCode() : 0);
94 return result; 101 return result;
95 } 102 }
96 103
@@ -105,7 +112,9 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H @@ -105,7 +112,9 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
105 if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false; 112 if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false;
106 if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false; 113 if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false;
107 if (title != null ? !title.equals(that.title) : that.title != null) return false; 114 if (title != null ? !title.equals(that.title) : that.title != null) return false;
108 - return Arrays.equals(image, that.image); 115 + if (image != null ? !image.equals(that.image) : that.image != null) return false;
  116 + if (description != null ? !description.equals(that.image) : that.description != null) return false;
  117 + return true;
109 } 118 }
110 119
111 @Override 120 @Override
@@ -114,7 +123,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H @@ -114,7 +123,8 @@ public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> implements H
114 sb.append("tenantId=").append(tenantId); 123 sb.append("tenantId=").append(tenantId);
115 sb.append(", alias='").append(alias).append('\''); 124 sb.append(", alias='").append(alias).append('\'');
116 sb.append(", title='").append(title).append('\''); 125 sb.append(", title='").append(title).append('\'');
117 - sb.append(", image=").append(Arrays.toString(image)); 126 + sb.append(", image='").append(image).append('\'');
  127 + sb.append(", description='").append(description).append('\'');
118 sb.append('}'); 128 sb.append('}');
119 return sb.toString(); 129 return sb.toString();
120 } 130 }
@@ -304,6 +304,7 @@ public class ModelConstants { @@ -304,6 +304,7 @@ public class ModelConstants {
304 public static final String WIDGETS_BUNDLE_ALIAS_PROPERTY = ALIAS_PROPERTY; 304 public static final String WIDGETS_BUNDLE_ALIAS_PROPERTY = ALIAS_PROPERTY;
305 public static final String WIDGETS_BUNDLE_TITLE_PROPERTY = TITLE_PROPERTY; 305 public static final String WIDGETS_BUNDLE_TITLE_PROPERTY = TITLE_PROPERTY;
306 public static final String WIDGETS_BUNDLE_IMAGE_PROPERTY = "image"; 306 public static final String WIDGETS_BUNDLE_IMAGE_PROPERTY = "image";
  307 + public static final String WIDGETS_BUNDLE_DESCRIPTION = "description";
307 308
308 public static final String WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_search_text"; 309 public static final String WIDGETS_BUNDLE_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_search_text";
309 public static final String WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_alias"; 310 public static final String WIDGETS_BUNDLE_BY_TENANT_AND_ALIAS_COLUMN_FAMILY_NAME = "widgets_bundle_by_tenant_and_alias";
@@ -316,6 +317,8 @@ public class ModelConstants { @@ -316,6 +317,8 @@ public class ModelConstants {
316 public static final String WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY = "bundle_alias"; 317 public static final String WIDGET_TYPE_BUNDLE_ALIAS_PROPERTY = "bundle_alias";
317 public static final String WIDGET_TYPE_ALIAS_PROPERTY = ALIAS_PROPERTY; 318 public static final String WIDGET_TYPE_ALIAS_PROPERTY = ALIAS_PROPERTY;
318 public static final String WIDGET_TYPE_NAME_PROPERTY = "name"; 319 public static final String WIDGET_TYPE_NAME_PROPERTY = "name";
  320 + public static final String WIDGET_TYPE_IMAGE_PROPERTY = "image";
  321 + public static final String WIDGET_TYPE_DESCRIPTION_PROPERTY = "description";
319 public static final String WIDGET_TYPE_DESCRIPTOR_PROPERTY = "descriptor"; 322 public static final String WIDGET_TYPE_DESCRIPTOR_PROPERTY = "descriptor";
320 323
321 public static final String WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME = "widget_type_by_tenant_and_aliases"; 324 public static final String WIDGET_TYPE_BY_TENANT_AND_ALIASES_COLUMN_FAMILY_NAME = "widget_type_by_tenant_and_aliases";
@@ -52,6 +52,12 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement @@ -52,6 +52,12 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement
52 @Column(name = ModelConstants.WIDGET_TYPE_NAME_PROPERTY) 52 @Column(name = ModelConstants.WIDGET_TYPE_NAME_PROPERTY)
53 private String name; 53 private String name;
54 54
  55 + @Column(name = ModelConstants.WIDGET_TYPE_IMAGE_PROPERTY)
  56 + private String image;
  57 +
  58 + @Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTION_PROPERTY)
  59 + private String description;
  60 +
55 @Type(type="json") 61 @Type(type="json")
56 @Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY) 62 @Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY)
57 private JsonNode descriptor; 63 private JsonNode descriptor;
@@ -71,6 +77,8 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement @@ -71,6 +77,8 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement
71 this.bundleAlias = widgetType.getBundleAlias(); 77 this.bundleAlias = widgetType.getBundleAlias();
72 this.alias = widgetType.getAlias(); 78 this.alias = widgetType.getAlias();
73 this.name = widgetType.getName(); 79 this.name = widgetType.getName();
  80 + this.image = widgetType.getImage();
  81 + this.description = widgetType.getDescription();
74 this.descriptor = widgetType.getDescriptor(); 82 this.descriptor = widgetType.getDescriptor();
75 } 83 }
76 84
@@ -84,6 +92,8 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement @@ -84,6 +92,8 @@ public final class WidgetTypeEntity extends BaseSqlEntity<WidgetType> implement
84 widgetType.setBundleAlias(bundleAlias); 92 widgetType.setBundleAlias(bundleAlias);
85 widgetType.setAlias(alias); 93 widgetType.setAlias(alias);
86 widgetType.setName(name); 94 widgetType.setName(name);
  95 + widgetType.setImage(image);
  96 + widgetType.setDescription(description);
87 widgetType.setDescriptor(descriptor); 97 widgetType.setDescriptor(descriptor);
88 return widgetType; 98 return widgetType;
89 } 99 }
@@ -48,6 +48,12 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl @@ -48,6 +48,12 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl
48 @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) 48 @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY)
49 private String searchText; 49 private String searchText;
50 50
  51 + @Column(name = ModelConstants.WIDGETS_BUNDLE_IMAGE_PROPERTY)
  52 + private String image;
  53 +
  54 + @Column(name = ModelConstants.WIDGETS_BUNDLE_DESCRIPTION)
  55 + private String description;
  56 +
51 public WidgetsBundleEntity() { 57 public WidgetsBundleEntity() {
52 super(); 58 super();
53 } 59 }
@@ -62,6 +68,8 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl @@ -62,6 +68,8 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl
62 } 68 }
63 this.alias = widgetsBundle.getAlias(); 69 this.alias = widgetsBundle.getAlias();
64 this.title = widgetsBundle.getTitle(); 70 this.title = widgetsBundle.getTitle();
  71 + this.image = widgetsBundle.getImage();
  72 + this.description = widgetsBundle.getDescription();
65 } 73 }
66 74
67 @Override 75 @Override
@@ -83,6 +91,8 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl @@ -83,6 +91,8 @@ public final class WidgetsBundleEntity extends BaseSqlEntity<WidgetsBundle> impl
83 } 91 }
84 widgetsBundle.setAlias(alias); 92 widgetsBundle.setAlias(alias);
85 widgetsBundle.setTitle(title); 93 widgetsBundle.setTitle(title);
  94 + widgetsBundle.setImage(image);
  95 + widgetsBundle.setDescription(description);
86 return widgetsBundle; 96 return widgetsBundle;
87 } 97 }
88 } 98 }
@@ -290,7 +290,9 @@ CREATE TABLE IF NOT EXISTS widget_type ( @@ -290,7 +290,9 @@ CREATE TABLE IF NOT EXISTS widget_type (
290 bundle_alias varchar(255), 290 bundle_alias varchar(255),
291 descriptor varchar(1000000), 291 descriptor varchar(1000000),
292 name varchar(255), 292 name varchar(255),
293 - tenant_id uuid 293 + tenant_id uuid,
  294 + image varchar(1000000),
  295 + description varchar(255)
294 ); 296 );
295 297
296 CREATE TABLE IF NOT EXISTS widgets_bundle ( 298 CREATE TABLE IF NOT EXISTS widgets_bundle (
@@ -299,7 +301,9 @@ CREATE TABLE IF NOT EXISTS widgets_bundle ( @@ -299,7 +301,9 @@ CREATE TABLE IF NOT EXISTS widgets_bundle (
299 alias varchar(255), 301 alias varchar(255),
300 search_text varchar(255), 302 search_text varchar(255),
301 tenant_id uuid, 303 tenant_id uuid,
302 - title varchar(255) 304 + title varchar(255),
  305 + image varchar(1000000),
  306 + description varchar(255)
303 ); 307 );
304 308
305 CREATE TABLE IF NOT EXISTS entity_view ( 309 CREATE TABLE IF NOT EXISTS entity_view (
@@ -315,7 +315,9 @@ CREATE TABLE IF NOT EXISTS widget_type ( @@ -315,7 +315,9 @@ CREATE TABLE IF NOT EXISTS widget_type (
315 bundle_alias varchar(255), 315 bundle_alias varchar(255),
316 descriptor varchar(1000000), 316 descriptor varchar(1000000),
317 name varchar(255), 317 name varchar(255),
318 - tenant_id uuid 318 + tenant_id uuid,
  319 + image varchar(1000000),
  320 + description varchar(255)
319 ); 321 );
320 322
321 CREATE TABLE IF NOT EXISTS widgets_bundle ( 323 CREATE TABLE IF NOT EXISTS widgets_bundle (
@@ -324,7 +326,9 @@ CREATE TABLE IF NOT EXISTS widgets_bundle ( @@ -324,7 +326,9 @@ CREATE TABLE IF NOT EXISTS widgets_bundle (
324 alias varchar(255), 326 alias varchar(255),
325 search_text varchar(255), 327 search_text varchar(255),
326 tenant_id uuid, 328 tenant_id uuid,
327 - title varchar(255) 329 + title varchar(255),
  330 + image varchar(1000000),
  331 + description varchar(255)
328 ); 332 );
329 333
330 CREATE TABLE IF NOT EXISTS entity_view ( 334 CREATE TABLE IF NOT EXISTS entity_view (
@@ -144,6 +144,8 @@ export class WidgetService { @@ -144,6 +144,8 @@ export class WidgetService {
144 typeAlias: widgetTypeInfo.alias, 144 typeAlias: widgetTypeInfo.alias,
145 type: widgetTypeInfo.type, 145 type: widgetTypeInfo.type,
146 title: widgetTypeInfo.widgetName, 146 title: widgetTypeInfo.widgetName,
  147 + image: widgetTypeInfo.image,
  148 + description: widgetTypeInfo.description,
147 sizeX, 149 sizeX,
148 sizeY, 150 sizeY,
149 row: top, 151 row: top,
@@ -44,6 +44,8 @@ export class WidgetEditorDashboardResolver implements Resolve<Dashboard> { @@ -44,6 +44,8 @@ export class WidgetEditorDashboardResolver implements Resolve<Dashboard> {
44 typeAlias: 'customWidget', 44 typeAlias: 'customWidget',
45 type: editWidgetInfo.type, 45 type: editWidgetInfo.type,
46 title: 'My widget', 46 title: 'My widget',
  47 + image: null,
  48 + description: null,
47 sizeX: editWidgetInfo.sizeX * 2, 49 sizeX: editWidgetInfo.sizeX * 2,
48 sizeY: editWidgetInfo.sizeY * 2, 50 sizeY: editWidgetInfo.sizeY * 2,
49 row: 2, 51 row: 2,
@@ -469,6 +469,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI @@ -469,6 +469,8 @@ export class AttributeTableComponent extends PageComponent implements AfterViewI
469 typeAlias: widgetInfo.alias, 469 typeAlias: widgetInfo.alias,
470 type: widgetInfo.type, 470 type: widgetInfo.type,
471 title: widgetInfo.widgetName, 471 title: widgetInfo.widgetName,
  472 + image: widgetInfo.image,
  473 + description: widgetInfo.description,
472 sizeX, 474 sizeX,
473 sizeY, 475 sizeY,
474 row: 0, 476 row: 0,
@@ -253,7 +253,7 @@ @@ -253,7 +253,7 @@
253 fxFlex 253 fxFlex
254 required 254 required
255 [selectFirstBundle]="false" 255 [selectFirstBundle]="false"
256 - [ngModel]="widgetsBundle" 256 + [(ngModel)]="widgetsBundle"
257 (ngModelChange)="widgetsBundle = $event"> 257 (ngModelChange)="widgetsBundle = $event">
258 </tb-widgets-bundle-select> 258 </tb-widgets-bundle-select>
259 </div> 259 </div>
@@ -261,6 +261,7 @@ @@ -261,6 +261,7 @@
261 <tb-dashboard-widget-select *ngIf="isAddingWidget" 261 <tb-dashboard-widget-select *ngIf="isAddingWidget"
262 [aliasController]="dashboardCtx.aliasController" 262 [aliasController]="dashboardCtx.aliasController"
263 [widgetsBundle]="widgetsBundle" 263 [widgetsBundle]="widgetsBundle"
  264 + (widgetsBundleSelected)="widgetBundleSelected($event)"
264 (widgetSelected)="addWidgetFromType($event)"> 265 (widgetSelected)="addWidgetFromType($event)">
265 </tb-dashboard-widget-select> 266 </tb-dashboard-widget-select>
266 </tb-details-panel> 267 </tb-details-panel>
@@ -865,6 +865,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -865,6 +865,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
865 typeAlias: widgetTypeInfo.alias, 865 typeAlias: widgetTypeInfo.alias,
866 type: widgetTypeInfo.type, 866 type: widgetTypeInfo.type,
867 title: 'New widget', 867 title: 'New widget',
  868 + image: null,
  869 + description: null,
868 sizeX: widgetTypeInfo.sizeX, 870 sizeX: widgetTypeInfo.sizeX,
869 sizeY: widgetTypeInfo.sizeY, 871 sizeY: widgetTypeInfo.sizeY,
870 config, 872 config,
@@ -1111,4 +1113,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -1111,4 +1113,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
1111 } 1113 }
1112 return widgetContextActions; 1114 return widgetContextActions;
1113 } 1115 }
  1116 +
  1117 + widgetBundleSelected(bundle: WidgetsBundle){
  1118 + this.widgetsBundle = bundle;
  1119 + }
1114 } 1120 }
@@ -15,75 +15,47 @@ @@ -15,75 +15,47 @@
15 limitations under the License. 15 limitations under the License.
16 16
17 --> 17 -->
18 -<div>  
19 - <mat-tab-group *ngIf="hasWidgetTypes()" class="tb-absolute-fill" fxFlex>  
20 - <mat-tab *ngIf="timeseriesWidgetTypes.length" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">  
21 - <tb-dashboard [aliasController]="aliasController"  
22 - [widgets]="timeseriesWidgetTypes"  
23 - [widgetLayouts]="{}"  
24 - [isEdit]="false"  
25 - [isMobile]="true"  
26 - [disableWidgetInteraction]="true"  
27 - [isEditActionEnabled]="false"  
28 - [isExportActionEnabled]="false"  
29 - [isRemoveActionEnabled]="false"  
30 - [callbacks]="callbacks"></tb-dashboard>  
31 - </mat-tab>  
32 - <mat-tab *ngIf="latestWidgetTypes.length" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">  
33 - <tb-dashboard [aliasController]="aliasController"  
34 - [widgets]="latestWidgetTypes"  
35 - [widgetLayouts]="{}"  
36 - [isEdit]="false"  
37 - [isMobile]="true"  
38 - [disableWidgetInteraction]="true"  
39 - [isEditActionEnabled]="false"  
40 - [isExportActionEnabled]="false"  
41 - [isRemoveActionEnabled]="false"  
42 - [callbacks]="callbacks"></tb-dashboard>  
43 - </mat-tab>  
44 - <mat-tab *ngIf="rpcWidgetTypes.length" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">  
45 - <tb-dashboard [aliasController]="aliasController"  
46 - [widgets]="rpcWidgetTypes"  
47 - [widgetLayouts]="{}"  
48 - [isEdit]="false"  
49 - [isMobile]="true"  
50 - [disableWidgetInteraction]="true"  
51 - [isEditActionEnabled]="false"  
52 - [isExportActionEnabled]="false"  
53 - [isRemoveActionEnabled]="false"  
54 - [callbacks]="callbacks"></tb-dashboard>  
55 - </mat-tab>  
56 - <mat-tab *ngIf="alarmWidgetTypes.length" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">  
57 - <tb-dashboard [aliasController]="aliasController"  
58 - [widgets]="alarmWidgetTypes"  
59 - [widgetLayouts]="{}"  
60 - [isEdit]="false"  
61 - [isMobile]="true"  
62 - [disableWidgetInteraction]="true"  
63 - [isEditActionEnabled]="false"  
64 - [isExportActionEnabled]="false"  
65 - [isRemoveActionEnabled]="false"  
66 - [callbacks]="callbacks"></tb-dashboard>  
67 - </mat-tab>  
68 - <mat-tab *ngIf="staticWidgetTypes.length" style="height: 100%;" label="{{ 'widget.static' | translate }}">  
69 - <tb-dashboard [aliasController]="aliasController"  
70 - [widgets]="staticWidgetTypes"  
71 - [widgetLayouts]="{}"  
72 - [isEdit]="false"  
73 - [isMobile]="true"  
74 - [disableWidgetInteraction]="true"  
75 - [isEditActionEnabled]="false"  
76 - [isExportActionEnabled]="false"  
77 - [isRemoveActionEnabled]="false"  
78 - [callbacks]="callbacks"></tb-dashboard>  
79 - </mat-tab>  
80 - </mat-tab-group> 18 +<div class="widget-select">
  19 + <div *ngIf="hasWidgetTypes()">
  20 + <button mat-raised-button (click)="backToSelectWidgetsBundle()" style="margin-bottom: 12px">
  21 + <mat-icon class="material-icons">undo</mat-icon>
  22 + {{ 'widget.all-bundles' | translate }}
  23 + </button>
  24 + <div fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
  25 + <div *ngFor="let widget of widgets" class="mat-card-container">
  26 + <mat-card fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="onWidgetClicked($event, widget)">
  27 + <div fxFlex="45">
  28 + <img class="preview" src="https://material.angular.io/assets/img/examples/shiba2.jpg" alt="{{ widget.title }}">
  29 + </div>
  30 + <div fxFlex fxLayout="column">
  31 + <mat-card-title>{{widget.title}}</mat-card-title>
  32 + <mat-card-subtitle>{{ 'widget.' + widget.type | translate }}</mat-card-subtitle>
  33 + <mat-card-content *ngIf="widgetsBundle.description">
  34 + {{ widget.description }}
  35 + </mat-card-content>
  36 + </div>
  37 + </mat-card>
  38 + </div>
  39 + </div>
  40 + </div>
81 <span translate *ngIf="widgetsBundle && !hasWidgetTypes()" 41 <span translate *ngIf="widgetsBundle && !hasWidgetTypes()"
82 style="display: flex;" 42 style="display: flex;"
83 fxLayoutAlign="center center" 43 fxLayoutAlign="center center"
84 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span> 44 class="mat-headline tb-absolute-fill">widgets-bundle.empty</span>
85 - <span translate *ngIf="!widgetsBundle"  
86 - style="display: flex;"  
87 - fxLayoutAlign="center center"  
88 - class="mat-headline tb-absolute-fill">widget.select-widgets-bundle</span> 45 + <div *ngIf="!widgetsBundle" fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
  46 + <div *ngFor="let widgetsBundle of widgetsBundles$ | async" class="mat-card-container">
  47 + <mat-card fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="selectBundle($event, widgetsBundle)">
  48 + <div fxFlex="45" *ngIf="isSystem(widgetsBundle)">
  49 + <img class="preview" src="https://material.angular.io/assets/img/examples/shiba2.jpg" alt="{{ widgetsBundle.title }}">
  50 + </div>
  51 + <div fxFlex fxLayout="column">
  52 + <mat-card-title>{{ widgetsBundle.title }}</mat-card-title>
  53 + <mat-card-subtitle *ngIf="isSystem(widgetsBundle)" translate>widgets-bundle.system</mat-card-subtitle>
  54 + <mat-card-content *ngIf="widgetsBundle.description">
  55 + {{ widgetsBundle.description }}
  56 + </mat-card-content>
  57 + </div>
  58 + </mat-card>
  59 + </div>
  60 + </div>
89 </div> 61 </div>
@@ -13,10 +13,41 @@ @@ -13,10 +13,41 @@
13 * See the License for the specific language governing permissions and 13 * See the License for the specific language governing permissions and
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 -:host ::ng-deep {  
17 - .mat-tab-group {  
18 - .mat-tab-body-wrapper {  
19 - height: 100%; 16 +@import '../../../../../scss/constants';
  17 +
  18 +:host{
  19 + .widget-select {
  20 + min-height: 100%;
  21 + padding: 12px 0 12px 12px;
  22 + background-color: #cfd8dc;
  23 +
  24 + .mat-card-container {
  25 + flex: 0 0 100%;
  26 + max-width: 100%;
  27 +
  28 + .mat-card {
  29 + cursor: pointer;
  30 +
  31 + .preview {
  32 + max-width: 100%;
  33 + max-height: 100%;
  34 + object-fit: contain;
  35 + }
  36 + }
  37 + }
  38 +
  39 + @media #{$mat-gt-xs} {
  40 + .mat-card-container {
  41 + flex: 0 0 50%;
  42 + max-width: 50%;
  43 + }
  44 + }
  45 +
  46 + @media screen and (min-width: 2000px) {
  47 + .mat-card-container {
  48 + flex: 0 0 33.333333%;
  49 + max-width: 33.333333%;
  50 + }
20 } 51 }
21 } 52 }
22 } 53 }
@@ -19,9 +19,10 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; @@ -19,9 +19,10 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
19 import { IAliasController } from '@core/api/widget-api.models'; 19 import { IAliasController } from '@core/api/widget-api.models';
20 import { NULL_UUID } from '@shared/models/id/has-uuid'; 20 import { NULL_UUID } from '@shared/models/id/has-uuid';
21 import { WidgetService } from '@core/http/widget.service'; 21 import { WidgetService } from '@core/http/widget.service';
22 -import { Widget, widgetType } from '@shared/models/widget.models'; 22 +import { Widget } from '@shared/models/widget.models';
23 import { toWidgetInfo } from '@home/models/widget-component.models'; 23 import { toWidgetInfo } from '@home/models/widget-component.models';
24 -import { DashboardCallbacks } from '../../models/dashboard-component.models'; 24 +import { share } from 'rxjs/operators';
  25 +import { Observable } from 'rxjs';
25 26
26 @Component({ 27 @Component({
27 selector: 'tb-dashboard-widget-select', 28 selector: 'tb-dashboard-widget-select',
@@ -39,20 +40,20 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { @@ -39,20 +40,20 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges {
39 @Output() 40 @Output()
40 widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>(); 41 widgetSelected: EventEmitter<Widget> = new EventEmitter<Widget>();
41 42
42 - timeseriesWidgetTypes: Array<Widget> = [];  
43 - latestWidgetTypes: Array<Widget> = [];  
44 - rpcWidgetTypes: Array<Widget> = [];  
45 - alarmWidgetTypes: Array<Widget> = [];  
46 - staticWidgetTypes: Array<Widget> = []; 43 + @Output()
  44 + widgetsBundleSelected: EventEmitter<WidgetsBundle> = new EventEmitter<WidgetsBundle>();
  45 +
  46 + widgets: Array<Widget> = [];
47 47
48 - callbacks: DashboardCallbacks = {  
49 - onWidgetClicked: this.onWidgetClicked.bind(this)  
50 - }; 48 + widgetsBundles$: Observable<Array<WidgetsBundle>>;
51 49
52 constructor(private widgetsService: WidgetService) { 50 constructor(private widgetsService: WidgetService) {
53 } 51 }
54 52
55 ngOnInit(): void { 53 ngOnInit(): void {
  54 + this.widgetsBundles$ = this.widgetsService.getAllWidgetsBundles().pipe(
  55 + share()
  56 + );
56 } 57 }
57 58
58 ngOnChanges(changes: SimpleChanges): void { 59 ngOnChanges(changes: SimpleChanges): void {
@@ -67,11 +68,7 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { @@ -67,11 +68,7 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges {
67 } 68 }
68 69
69 private loadLibrary() { 70 private loadLibrary() {
70 - this.timeseriesWidgetTypes.length = 0;  
71 - this.latestWidgetTypes.length = 0;  
72 - this.rpcWidgetTypes.length = 0;  
73 - this.alarmWidgetTypes.length = 0;  
74 - this.staticWidgetTypes.length = 0; 71 + this.widgets.length = 0;
75 const bundleAlias = this.widgetsBundle.alias; 72 const bundleAlias = this.widgetsBundle.alias;
76 const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID; 73 const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
77 this.widgetsService.getBundleWidgetTypes(bundleAlias, 74 this.widgetsService.getBundleWidgetTypes(bundleAlias,
@@ -88,6 +85,8 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { @@ -88,6 +85,8 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges {
88 typeAlias: widgetTypeInfo.alias, 85 typeAlias: widgetTypeInfo.alias,
89 type: widgetTypeInfo.type, 86 type: widgetTypeInfo.type,
90 title: widgetTypeInfo.widgetName, 87 title: widgetTypeInfo.widgetName,
  88 + image: widgetTypeInfo.image,
  89 + description: widgetTypeInfo.description,
91 sizeX: widgetTypeInfo.sizeX, 90 sizeX: widgetTypeInfo.sizeX,
92 sizeY: widgetTypeInfo.sizeY, 91 sizeY: widgetTypeInfo.sizeY,
93 row: top, 92 row: top,
@@ -95,39 +94,35 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges { @@ -95,39 +94,35 @@ export class DashboardWidgetSelectComponent implements OnInit, OnChanges {
95 config: JSON.parse(widgetTypeInfo.defaultConfig) 94 config: JSON.parse(widgetTypeInfo.defaultConfig)
96 }; 95 };
97 widget.config.title = widgetTypeInfo.widgetName; 96 widget.config.title = widgetTypeInfo.widgetName;
98 - switch (widgetTypeInfo.type) {  
99 - case widgetType.timeseries:  
100 - this.timeseriesWidgetTypes.push(widget);  
101 - break;  
102 - case widgetType.latest:  
103 - this.latestWidgetTypes.push(widget);  
104 - break;  
105 - case widgetType.rpc:  
106 - this.rpcWidgetTypes.push(widget);  
107 - break;  
108 - case widgetType.alarm:  
109 - this.alarmWidgetTypes.push(widget);  
110 - break;  
111 - case widgetType.static:  
112 - this.staticWidgetTypes.push(widget);  
113 - break;  
114 - } 97 + this.widgets.push(widget);
115 top += widget.sizeY; 98 top += widget.sizeY;
116 }); 99 });
117 } 100 }
118 ); 101 );
119 } 102 }
120 103
121 - hasWidgetTypes() {  
122 - return this.timeseriesWidgetTypes.length > 0 ||  
123 - this.latestWidgetTypes.length > 0 ||  
124 - this.rpcWidgetTypes.length > 0 ||  
125 - this.alarmWidgetTypes.length > 0 ||  
126 - this.staticWidgetTypes.length > 0; 104 + hasWidgetTypes(): boolean {
  105 + return this.widgets.length > 0;
127 } 106 }
128 107
129 - private onWidgetClicked($event: Event, widget: Widget, index: number): void { 108 + onWidgetClicked($event: Event, widget: Widget): void {
130 this.widgetSelected.emit(widget); 109 this.widgetSelected.emit(widget);
131 } 110 }
132 111
  112 + isSystem(item: WidgetsBundle): boolean {
  113 + return item && item.tenantId.id === NULL_UUID;
  114 + }
  115 +
  116 + selectBundle($event: Event, bundle: WidgetsBundle) {
  117 + $event.preventDefault();
  118 + this.widgetsBundle = bundle;
  119 + this.widgetsBundleSelected.emit(bundle);
  120 + }
  121 +
  122 + backToSelectWidgetsBundle() {
  123 + this.widgetsBundle = null;
  124 + this.widgets.length = 0;
  125 + this.widgetsBundleSelected.emit(null);
  126 + }
  127 +
133 } 128 }
@@ -25,7 +25,6 @@ import { @@ -25,7 +25,6 @@ import {
25 Injector, 25 Injector,
26 Input, 26 Input,
27 OnDestroy, 27 OnDestroy,
28 - OnInit,  
29 Output, 28 Output,
30 QueryList, 29 QueryList,
31 ViewChild, 30 ViewChild,
@@ -52,7 +51,7 @@ import { deepClone } from '@core/utils'; @@ -52,7 +51,7 @@ import { deepClone } from '@core/utils';
52 styleUrls: ['./entity-details-panel.component.scss'], 51 styleUrls: ['./entity-details-panel.component.scss'],
53 changeDetection: ChangeDetectionStrategy.OnPush 52 changeDetection: ChangeDetectionStrategy.OnPush
54 }) 53 })
55 -export class EntityDetailsPanelComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { 54 +export class EntityDetailsPanelComponent extends PageComponent implements AfterViewInit, OnDestroy {
56 55
57 @Output() 56 @Output()
58 closeEntityDetails = new EventEmitter<void>(); 57 closeEntityDetails = new EventEmitter<void>();
@@ -140,10 +139,6 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit @@ -140,10 +139,6 @@ export class EntityDetailsPanelComponent extends PageComponent implements OnInit
140 return this.isEditValue; 139 return this.isEditValue;
141 } 140 }
142 141
143 - ngOnInit(): void {  
144 - this.init();  
145 - }  
146 -  
147 private init() { 142 private init() {
148 this.translations = this.entitiesTableConfig.entityTranslations; 143 this.translations = this.entitiesTableConfig.entityTranslations;
149 this.resources = this.entitiesTableConfig.entityResources; 144 this.resources = this.entitiesTableConfig.entityResources;
@@ -104,6 +104,8 @@ export class WidgetComponentService { @@ -104,6 +104,8 @@ export class WidgetComponentService {
104 controllerScript: this.utils.editWidgetInfo.controllerScript, 104 controllerScript: this.utils.editWidgetInfo.controllerScript,
105 settingsSchema: this.utils.editWidgetInfo.settingsSchema, 105 settingsSchema: this.utils.editWidgetInfo.settingsSchema,
106 dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema, 106 dataKeySettingsSchema: this.utils.editWidgetInfo.dataKeySettingsSchema,
  107 + image: this.utils.editWidgetInfo.image,
  108 + description: this.utils.editWidgetInfo.description,
107 defaultConfig: this.utils.editWidgetInfo.defaultConfig 109 defaultConfig: this.utils.editWidgetInfo.defaultConfig
108 }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle' 110 }, new WidgetTypeId('1'), new TenantId( NULL_UUID ), 'customWidgetBundle'
109 ); 111 );
@@ -347,6 +347,8 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri @@ -347,6 +347,8 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
347 alias: string; 347 alias: string;
348 typeSettingsSchema?: string | any; 348 typeSettingsSchema?: string | any;
349 typeDataKeySettingsSchema?: string | any; 349 typeDataKeySettingsSchema?: string | any;
  350 + image: string;
  351 + description: string;
350 componentFactory?: ComponentFactory<IDynamicWidgetComponent>; 352 componentFactory?: ComponentFactory<IDynamicWidgetComponent>;
351 } 353 }
352 354
@@ -375,6 +377,8 @@ export const MissingWidgetType: WidgetInfo = { @@ -375,6 +377,8 @@ export const MissingWidgetType: WidgetInfo = {
375 controllerScript: 'self.onInit = function() {}', 377 controllerScript: 'self.onInit = function() {}',
376 settingsSchema: '{}\n', 378 settingsSchema: '{}\n',
377 dataKeySettingsSchema: '{}\n', 379 dataKeySettingsSchema: '{}\n',
  380 + image: null,
  381 + description: null,
378 defaultConfig: '{\n' + 382 defaultConfig: '{\n' +
379 '"title": "Widget type not found",\n' + 383 '"title": "Widget type not found",\n' +
380 '"datasources": [],\n' + 384 '"datasources": [],\n' +
@@ -398,6 +402,8 @@ export const ErrorWidgetType: WidgetInfo = { @@ -398,6 +402,8 @@ export const ErrorWidgetType: WidgetInfo = {
398 controllerScript: 'self.onInit = function() {}', 402 controllerScript: 'self.onInit = function() {}',
399 settingsSchema: '{}\n', 403 settingsSchema: '{}\n',
400 dataKeySettingsSchema: '{}\n', 404 dataKeySettingsSchema: '{}\n',
  405 + image: null,
  406 + description: null,
401 defaultConfig: '{\n' + 407 defaultConfig: '{\n' +
402 '"title": "Widget failed to load",\n' + 408 '"title": "Widget failed to load",\n' +
403 '"datasources": [],\n' + 409 '"datasources": [],\n' +
@@ -424,6 +430,8 @@ export interface WidgetTypeInstance { @@ -424,6 +430,8 @@ export interface WidgetTypeInstance {
424 export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo { 430 export function toWidgetInfo(widgetTypeEntity: WidgetType): WidgetInfo {
425 return { 431 return {
426 widgetName: widgetTypeEntity.name, 432 widgetName: widgetTypeEntity.name,
  433 + image: widgetTypeEntity.image,
  434 + description: widgetTypeEntity.description,
427 alias: widgetTypeEntity.alias, 435 alias: widgetTypeEntity.alias,
428 type: widgetTypeEntity.descriptor.type, 436 type: widgetTypeEntity.descriptor.type,
429 sizeX: widgetTypeEntity.descriptor.sizeX, 437 sizeX: widgetTypeEntity.descriptor.sizeX,
@@ -457,6 +465,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId: @@ -457,6 +465,8 @@ export function toWidgetType(widgetInfo: WidgetInfo, id: WidgetTypeId, tenantId:
457 bundleAlias, 465 bundleAlias,
458 alias: widgetInfo.alias, 466 alias: widgetInfo.alias,
459 name: widgetInfo.widgetName, 467 name: widgetInfo.widgetName,
  468 + image: widgetInfo.image,
  469 + description: widgetInfo.description,
460 descriptor 470 descriptor
461 }; 471 };
462 } 472 }
@@ -117,7 +117,7 @@ @@ -117,7 +117,7 @@
117 <div #topPanel class="tb-split tb-split-vertical"> 117 <div #topPanel class="tb-split tb-split-vertical">
118 <div #topLeftPanel class="tb-split tb-content"> 118 <div #topLeftPanel class="tb-split tb-content">
119 <mat-tab-group selectedIndex="1" dynamicHeight="true" style="width: 100%; height: 100%;"> 119 <mat-tab-group selectedIndex="1" dynamicHeight="true" style="width: 100%; height: 100%;">
120 - <mat-tab label="{{ 'widget.resources' | translate }}" style="width: 100%; height: 100%;"> 120 + <mat-tab label="{{ 'widget.resources' | translate }}">
121 <div class="tb-resize-container" style="background-color: #fff;"> 121 <div class="tb-resize-container" style="background-color: #fff;">
122 <div class="mat-padding"> 122 <div class="mat-padding">
123 <div fxFlex fxLayout="row" style="max-height: 40px;" 123 <div fxFlex fxLayout="row" style="max-height: 40px;"
@@ -153,7 +153,7 @@ @@ -153,7 +153,7 @@
153 </div> 153 </div>
154 </div> 154 </div>
155 </mat-tab> 155 </mat-tab>
156 - <mat-tab label="{{ 'widget.html' | translate }}" style="width: 100%; height: 100%;"> 156 + <mat-tab label="{{ 'widget.html' | translate }}">
157 <div class="tb-resize-container" tb-fullscreen [fullscreen]="htmlFullscreen"> 157 <div class="tb-resize-container" tb-fullscreen [fullscreen]="htmlFullscreen">
158 <div class="tb-editor-area-title-panel"> 158 <div class="tb-editor-area-title-panel">
159 <button mat-button (click)="beautifyHtml()"> 159 <button mat-button (click)="beautifyHtml()">
@@ -169,7 +169,7 @@ @@ -169,7 +169,7 @@
169 <div #htmlInput></div> 169 <div #htmlInput></div>
170 </div> 170 </div>
171 </mat-tab> 171 </mat-tab>
172 - <mat-tab label="{{ 'widget.css' | translate }}" style="width: 100%; height: 100%;"> 172 + <mat-tab label="{{ 'widget.css' | translate }}">
173 <div class="tb-resize-container" tb-fullscreen [fullscreen]="cssFullscreen"> 173 <div class="tb-resize-container" tb-fullscreen [fullscreen]="cssFullscreen">
174 <div class="tb-editor-area-title-panel"> 174 <div class="tb-editor-area-title-panel">
175 <button mat-button (click)="beautifyCss()"> 175 <button mat-button (click)="beautifyCss()">
@@ -189,7 +189,7 @@ @@ -189,7 +189,7 @@
189 </div> 189 </div>
190 <div #topRightPanel class="tb-split tb-content"> 190 <div #topRightPanel class="tb-split tb-content">
191 <mat-tab-group dynamicHeight="true" style="width: 100%; height: 100%;"> 191 <mat-tab-group dynamicHeight="true" style="width: 100%; height: 100%;">
192 - <mat-tab label="{{ 'widget.settings-schema' | translate }}" style="width: 100%; height: 100%;"> 192 + <mat-tab label="{{ 'widget.settings-schema' | translate }}">
193 <div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen"> 193 <div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonSettingsFullscreen">
194 <div class="tb-editor-area-title-panel"> 194 <div class="tb-editor-area-title-panel">
195 <button mat-button (click)="beautifyJson()"> 195 <button mat-button (click)="beautifyJson()">
@@ -205,7 +205,7 @@ @@ -205,7 +205,7 @@
205 <div #settingsJsonInput></div> 205 <div #settingsJsonInput></div>
206 </div> 206 </div>
207 </mat-tab> 207 </mat-tab>
208 - <mat-tab label="{{ 'widget.datakey-settings-schema' | translate }}" style="width: 100%; height: 100%;"> 208 + <mat-tab label="{{ 'widget.datakey-settings-schema' | translate }}">
209 <div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonDataKeySettingsFullscreen"> 209 <div class="tb-resize-container" tb-fullscreen [fullscreen]="jsonDataKeySettingsFullscreen">
210 <div class="tb-editor-area-title-panel"> 210 <div class="tb-editor-area-title-panel">
211 <button mat-button (click)="beautifyDataKeyJson()"> 211 <button mat-button (click)="beautifyDataKeyJson()">
@@ -221,6 +221,25 @@ @@ -221,6 +221,25 @@
221 <div #dataKeySettingsJsonInput></div> 221 <div #dataKeySettingsJsonInput></div>
222 </div> 222 </div>
223 </mat-tab> 223 </mat-tab>
  224 + <mat-tab label="{{ 'widget.widget-settings' | translate }}">
  225 + <div class="tb-resize-container" style="background-color: #fff;">
  226 + <div class="mat-padding">
  227 + <tb-image-input fxFlex label="{{'widget.image-preview' | translate}}"
  228 + maxSizeByte="524288"
  229 + [(ngModel)]="widget.image"
  230 + (ngModelChange)="isDirty = true" >
  231 + </tb-image-input>
  232 + <mat-form-field class="mat-block">
  233 + <mat-label translate>widget.description</mat-label>
  234 + <textarea matInput #descriptionInput
  235 + [(ngModel)]="widget.description"
  236 + (ngModelChange)="isDirty = true"
  237 + rows="2" maxlength="255"></textarea>
  238 + <mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint>
  239 + </mat-form-field>
  240 + </div>
  241 + </div>
  242 + </mat-tab>
224 </mat-tab-group> 243 </mat-tab-group>
225 </div> 244 </div>
226 </div> 245 </div>
@@ -45,6 +45,16 @@ @@ -45,6 +45,16 @@
45 {{ 'widgets-bundle.title-required' | translate }} 45 {{ 'widgets-bundle.title-required' | translate }}
46 </mat-error> 46 </mat-error>
47 </mat-form-field> 47 </mat-form-field>
  48 + <tb-image-input fxFlex
  49 + label="{{'widgets-bundle.image-preview' | translate}}"
  50 + maxSizeByte="524288"
  51 + formControlName="image">
  52 + </tb-image-input>
  53 + <mat-form-field class="mat-block">
  54 + <mat-label translate>widgets-bundle.description</mat-label>
  55 + <textarea matInput formControlName="description" rows="2" maxlength="255" #descriptionInput></textarea>
  56 + <mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint>
  57 + </mat-form-field>
48 </fieldset> 58 </fieldset>
49 </form> 59 </form>
50 </div> 60 </div>
@@ -47,12 +47,18 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> { @@ -47,12 +47,18 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> {
47 buildForm(entity: WidgetsBundle): FormGroup { 47 buildForm(entity: WidgetsBundle): FormGroup {
48 return this.fb.group( 48 return this.fb.group(
49 { 49 {
50 - title: [entity ? entity.title : '', [Validators.required]] 50 + title: [entity ? entity.title : '', [Validators.required]],
  51 + image: [entity ? entity.image : ''],
  52 + description: [entity ? entity.description : '', Validators.maxLength(255)]
51 } 53 }
52 ); 54 );
53 } 55 }
54 56
55 updateForm(entity: WidgetsBundle) { 57 updateForm(entity: WidgetsBundle) {
56 - this.entityForm.patchValue({title: entity.title}); 58 + this.entityForm.patchValue({
  59 + title: entity.title,
  60 + image: entity.image,
  61 + description: entity.description
  62 + });
57 } 63 }
58 } 64 }
@@ -42,4 +42,5 @@ @@ -42,4 +42,5 @@
42 </div> 42 </div>
43 </div> 43 </div>
44 </ng-container> 44 </ng-container>
  45 + <div class="tb-hint" *ngIf="maxSizeByte" translate [translateParams]="{ size: maxSizeByte | fileSize}">dashboard.maximum-upload-file-size</div>
45 </div> 46 </div>
@@ -101,4 +101,8 @@ $previewSize: 100px !default; @@ -101,4 +101,8 @@ $previewSize: 100px !default;
101 } 101 }
102 } 102 }
103 } 103 }
  104 +
  105 + .tb-hint{
  106 + margin-top: 8px;
  107 + }
104 } 108 }
@@ -24,6 +24,9 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; @@ -24,6 +24,9 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion';
24 import { FlowDirective } from '@flowjs/ngx-flow'; 24 import { FlowDirective } from '@flowjs/ngx-flow';
25 import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; 25 import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
26 import { UtilsService } from '@core/services/utils.service'; 26 import { UtilsService } from '@core/services/utils.service';
  27 +import { DialogService } from '@core/services/dialog.service';
  28 +import { TranslateService } from '@ngx-translate/core';
  29 +import { FileSizePipe } from '@shared/pipe/file-size.pipe';
27 30
28 @Component({ 31 @Component({
29 selector: 'tb-image-input', 32 selector: 'tb-image-input',
@@ -42,6 +45,9 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, @@ -42,6 +45,9 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
42 @Input() 45 @Input()
43 label: string; 46 label: string;
44 47
  48 + @Input()
  49 + maxSizeByte: number;
  50 +
45 private requiredValue: boolean; 51 private requiredValue: boolean;
46 52
47 get required(): boolean { 53 get required(): boolean {
@@ -80,7 +86,10 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, @@ -80,7 +86,10 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
80 86
81 constructor(protected store: Store<AppState>, 87 constructor(protected store: Store<AppState>,
82 private utils: UtilsService, 88 private utils: UtilsService,
83 - private sanitizer: DomSanitizer) { 89 + private sanitizer: DomSanitizer,
  90 + private dialog: DialogService,
  91 + private translate: TranslateService,
  92 + private fileSize: FileSizePipe) {
84 super(store); 93 super(store);
85 } 94 }
86 95
@@ -88,6 +97,15 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit, @@ -88,6 +97,15 @@ export class ImageInputComponent extends PageComponent implements AfterViewInit,
88 this.autoUploadSubscription = this.flow.events$.subscribe(event => { 97 this.autoUploadSubscription = this.flow.events$.subscribe(event => {
89 if (event.type === 'fileAdded') { 98 if (event.type === 'fileAdded') {
90 const file = (event.event[0] as flowjs.FlowFile).file; 99 const file = (event.event[0] as flowjs.FlowFile).file;
  100 + if (this.maxSizeByte && this.maxSizeByte < file.size) {
  101 + this.dialog.alert(
  102 + this.translate.instant('dashboard.cannot-upload-file'),
  103 + this.translate.instant('dashboard.maximum-upload-file-size', {size: this.fileSize.transform(this.maxSizeByte)})
  104 + ).subscribe(
  105 + () => { }
  106 + );
  107 + return false;
  108 + }
91 const reader = new FileReader(); 109 const reader = new FileReader();
92 reader.onload = (loadEvent) => { 110 reader.onload = (loadEvent) => {
93 if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) { 111 if (typeof reader.result === 'string' && reader.result.startsWith('data:image/')) {
@@ -63,7 +63,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>( @@ -63,7 +63,7 @@ export const widgetTypesData = new Map<widgetType, WidgetTypeData>(
63 [ 63 [
64 widgetType.latest, 64 widgetType.latest,
65 { 65 {
66 - name: 'widget.latest-values', 66 + name: 'widget.latest',
67 icon: 'track_changes', 67 icon: 'track_changes',
68 configHelpLinkId: 'widgetsConfigLatest', 68 configHelpLinkId: 'widgetsConfigLatest',
69 template: { 69 template: {
@@ -169,6 +169,8 @@ export interface WidgetType extends BaseData<WidgetTypeId> { @@ -169,6 +169,8 @@ export interface WidgetType extends BaseData<WidgetTypeId> {
169 bundleAlias: string; 169 bundleAlias: string;
170 alias: string; 170 alias: string;
171 name: string; 171 name: string;
  172 + image: string;
  173 + description: string;
172 descriptor: WidgetTypeDescriptor; 174 descriptor: WidgetTypeDescriptor;
173 } 175 }
174 176
@@ -408,6 +410,8 @@ export interface Widget { @@ -408,6 +410,8 @@ export interface Widget {
408 sizeY: number; 410 sizeY: number;
409 row: number; 411 row: number;
410 col: number; 412 col: number;
  413 + image: string;
  414 + description: string;
411 config: WidgetConfig; 415 config: WidgetConfig;
412 } 416 }
413 417
@@ -426,7 +430,7 @@ export interface JsonSchema { @@ -426,7 +430,7 @@ export interface JsonSchema {
426 export interface JsonSettingsSchema { 430 export interface JsonSettingsSchema {
427 schema?: JsonSchema; 431 schema?: JsonSchema;
428 form?: any[]; 432 form?: any[];
429 - groupInfoes?: GroupInfo[] 433 + groupInfoes?: GroupInfo[];
430 } 434 }
431 435
432 export interface WidgetPosition { 436 export interface WidgetPosition {
@@ -23,4 +23,5 @@ export interface WidgetsBundle extends BaseData<WidgetsBundleId> { @@ -23,4 +23,5 @@ export interface WidgetsBundle extends BaseData<WidgetsBundleId> {
23 alias: string; 23 alias: string;
24 title: string; 24 title: string;
25 image: string; 25 image: string;
  26 + description: string;
26 } 27 }
  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 { Pipe, PipeTransform } from '@angular/core';
  18 +
  19 +type unit = 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB';
  20 +type unitPrecisionMap = {
  21 + [u in unit]: number;
  22 +};
  23 +
  24 +const defaultPrecisionMap: unitPrecisionMap = {
  25 + bytes: 0,
  26 + KB: 0,
  27 + MB: 1,
  28 + GB: 1,
  29 + TB: 2,
  30 + PB: 2
  31 +};
  32 +
  33 +@Pipe({ name: 'fileSize' })
  34 +export class FileSizePipe implements PipeTransform {
  35 + private readonly units: unit[] = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
  36 +
  37 + transform(bytes: number = 0, precision: number | unitPrecisionMap = defaultPrecisionMap): string {
  38 + if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) {
  39 + return '?';
  40 + }
  41 +
  42 + let unitIndex = 0;
  43 +
  44 + while (bytes >= 1024) {
  45 + bytes /= 1024;
  46 + unitIndex++;
  47 + }
  48 +
  49 + const unitSymbol = this.units[unitIndex];
  50 +
  51 + if (typeof precision === 'number') {
  52 + return `${bytes.toFixed(+precision)} ${unitSymbol}`;
  53 + }
  54 + return `${bytes.toFixed(precision[unitSymbol])} ${unitSymbol}`;
  55 + }
  56 +}
@@ -20,3 +20,4 @@ export * from './keyboard-shortcut.pipe'; @@ -20,3 +20,4 @@ export * from './keyboard-shortcut.pipe';
20 export * from './milliseconds-to-time-string.pipe'; 20 export * from './milliseconds-to-time-string.pipe';
21 export * from './nospace.pipe'; 21 export * from './nospace.pipe';
22 export * from './truncate.pipe'; 22 export * from './truncate.pipe';
  23 +export * from './file-size.pipe';
@@ -135,6 +135,7 @@ import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-g @@ -135,6 +135,7 @@ import { EntityGatewaySelectComponent } from '@shared/components/entity/entity-g
135 import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component'; 135 import { QueueTypeListComponent } from '@shared/components/queue/queue-type-list.component';
136 import { ContactComponent } from '@shared/components/contact.component'; 136 import { ContactComponent } from '@shared/components/contact.component';
137 import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component'; 137 import { TimezoneSelectComponent } from '@shared/components/time/timezone-select.component';
  138 +import { FileSizePipe } from '@shared/pipe/file-size.pipe';
138 139
139 @NgModule({ 140 @NgModule({
140 providers: [ 141 providers: [
@@ -144,6 +145,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select @@ -144,6 +145,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
144 HighlightPipe, 145 HighlightPipe,
145 TruncatePipe, 146 TruncatePipe,
146 TbJsonPipe, 147 TbJsonPipe,
  148 + FileSizePipe,
147 { 149 {
148 provide: FlowInjectionToken, 150 provide: FlowInjectionToken,
149 useValue: Flow 151 useValue: Flow
@@ -217,6 +219,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select @@ -217,6 +219,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
217 HighlightPipe, 219 HighlightPipe,
218 TruncatePipe, 220 TruncatePipe,
219 TbJsonPipe, 221 TbJsonPipe,
  222 + FileSizePipe,
220 KeyboardShortcutPipe, 223 KeyboardShortcutPipe,
221 TbJsonToStringDirective, 224 TbJsonToStringDirective,
222 JsonObjectEditDialogComponent, 225 JsonObjectEditDialogComponent,
@@ -381,6 +384,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select @@ -381,6 +384,7 @@ import { TimezoneSelectComponent } from '@shared/components/time/timezone-select
381 TruncatePipe, 384 TruncatePipe,
382 TbJsonPipe, 385 TbJsonPipe,
383 KeyboardShortcutPipe, 386 KeyboardShortcutPipe,
  387 + FileSizePipe,
384 TranslateModule, 388 TranslateModule,
385 JsonObjectEditDialogComponent, 389 JsonObjectEditDialogComponent,
386 HistorySelectorComponent, 390 HistorySelectorComponent,
@@ -2206,7 +2206,7 @@ @@ -2206,7 +2206,7 @@
2206 "timeseries": "Časové řady", 2206 "timeseries": "Časové řady",
2207 "search-data": "Vyhledat data", 2207 "search-data": "Vyhledat data",
2208 "no-data-found": "Žádná data nebyla nalezena", 2208 "no-data-found": "Žádná data nebyla nalezena",
2209 - "latest-values": "Poslední hodnoty", 2209 + "latest": "Poslední hodnoty",
2210 "rpc": "Ovládací widget", 2210 "rpc": "Ovládací widget",
2211 "alarm": "Widgety alarmu", 2211 "alarm": "Widgety alarmu",
2212 "static": "Statické widgety", 2212 "static": "Statické widgety",
@@ -1451,7 +1451,7 @@ @@ -1451,7 +1451,7 @@
1451 "timeseries": "Zeitreihe", 1451 "timeseries": "Zeitreihe",
1452 "search-data": "Daten suchen", 1452 "search-data": "Daten suchen",
1453 "no-data-found": "Keine Daten gefunden", 1453 "no-data-found": "Keine Daten gefunden",
1454 - "latest-values": "Neueste Werte", 1454 + "latest": "Neueste Werte",
1455 "rpc": "Steuerungswidget", 1455 "rpc": "Steuerungswidget",
1456 "alarm": "Alarm-Widget", 1456 "alarm": "Alarm-Widget",
1457 "static": "Statisches Widget", 1457 "static": "Statisches Widget",
@@ -2318,7 +2318,7 @@ @@ -2318,7 +2318,7 @@
2318 "timeseries": "Χρονική σειρά", 2318 "timeseries": "Χρονική σειρά",
2319 "search-data": "Αναζήτηση δεδομένων", 2319 "search-data": "Αναζήτηση δεδομένων",
2320 "no-data-found": "Δεν βρέθηκαν δεδομένα", 2320 "no-data-found": "Δεν βρέθηκαν δεδομένα",
2321 - "latest-values": "Τελευταίες αξίες", 2321 + "latest": "Τελευταίες αξίες",
2322 "rpc": "Έλεγχος Widget", 2322 "rpc": "Έλεγχος Widget",
2323 "alarm": "Alarm widget", 2323 "alarm": "Alarm widget",
2324 "static": "Στατικό widget", 2324 "static": "Στατικό widget",
@@ -688,6 +688,8 @@ @@ -688,6 +688,8 @@
688 "background-size-mode": "Background size mode", 688 "background-size-mode": "Background size mode",
689 "no-image": "No image selected", 689 "no-image": "No image selected",
690 "drop-image": "Drop an image or click to select a file to upload.", 690 "drop-image": "Drop an image or click to select a file to upload.",
  691 + "maximum-upload-file-size": "Maximum upload file size: {{ size }}",
  692 + "cannot-upload-file": "Cannot upload file",
691 "settings": "Settings", 693 "settings": "Settings",
692 "columns-count": "Columns count", 694 "columns-count": "Columns count",
693 "columns-count-required": "Columns count is required.", 695 "columns-count-required": "Columns count is required.",
@@ -2197,6 +2199,7 @@ @@ -2197,6 +2199,7 @@
2197 "widget": { 2199 "widget": {
2198 "widget-library": "Widgets Library", 2200 "widget-library": "Widgets Library",
2199 "widget-bundle": "Widgets Bundle", 2201 "widget-bundle": "Widgets Bundle",
  2202 + "all-bundles": "All bundles",
2200 "select-widgets-bundle": "Select widgets bundle", 2203 "select-widgets-bundle": "Select widgets bundle",
2201 "management": "Widget management", 2204 "management": "Widget management",
2202 "editor": "Widget Editor", 2205 "editor": "Widget Editor",
@@ -2209,7 +2212,7 @@ @@ -2209,7 +2212,7 @@
2209 "timeseries": "Time series", 2212 "timeseries": "Time series",
2210 "search-data": "Search data", 2213 "search-data": "Search data",
2211 "no-data-found": "No data found", 2214 "no-data-found": "No data found",
2212 - "latest-values": "Latest values", 2215 + "latest": "Latest values",
2213 "rpc": "Control widget", 2216 "rpc": "Control widget",
2214 "alarm": "Alarm widget", 2217 "alarm": "Alarm widget",
2215 "static": "Static widget", 2218 "static": "Static widget",
@@ -2236,6 +2239,9 @@ @@ -2236,6 +2239,9 @@
2236 "css": "CSS", 2239 "css": "CSS",
2237 "settings-schema": "Settings schema", 2240 "settings-schema": "Settings schema",
2238 "datakey-settings-schema": "Data key settings schema", 2241 "datakey-settings-schema": "Data key settings schema",
  2242 + "widget-settings": "Widget settings",
  2243 + "description": "Description",
  2244 + "image-preview": "Image preview",
2239 "javascript": "Javascript", 2245 "javascript": "Javascript",
2240 "js": "JS", 2246 "js": "JS",
2241 "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", 2247 "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?",
@@ -2278,6 +2284,8 @@ @@ -2278,6 +2284,8 @@
2278 "delete": "Delete widgets bundle", 2284 "delete": "Delete widgets bundle",
2279 "title": "Title", 2285 "title": "Title",
2280 "title-required": "Title is required.", 2286 "title-required": "Title is required.",
  2287 + "description": "Description",
  2288 + "image-preview": "Image preview",
2281 "add-widgets-bundle-text": "Add new widgets bundle", 2289 "add-widgets-bundle-text": "Add new widgets bundle",
2282 "no-widgets-bundles-text": "No widgets bundles found", 2290 "no-widgets-bundles-text": "No widgets bundles found",
2283 "empty": "Widgets bundle is empty", 2291 "empty": "Widgets bundle is empty",
@@ -2206,7 +2206,7 @@ @@ -2206,7 +2206,7 @@
2206 "timeseries": "Series de tiempo", 2206 "timeseries": "Series de tiempo",
2207 "search-data": "Buscar datos", 2207 "search-data": "Buscar datos",
2208 "no-data-found": "No se han encontrado datos", 2208 "no-data-found": "No se han encontrado datos",
2209 - "latest-values": "Últimos valores", 2209 + "latest": "Últimos valores",
2210 "rpc": "Widget de control", 2210 "rpc": "Widget de control",
2211 "alarm": "Widget de Alarma", 2211 "alarm": "Widget de Alarma",
2212 "static": "Widget estático", 2212 "static": "Widget estático",
@@ -1420,7 +1420,7 @@ @@ -1420,7 +1420,7 @@
1420 "timeseries": "سري هاي زماني", 1420 "timeseries": "سري هاي زماني",
1421 "search-data": "جستجوي داده", 1421 "search-data": "جستجوي داده",
1422 "no-data-found": "هيچ داده اي يافت نشد", 1422 "no-data-found": "هيچ داده اي يافت نشد",
1423 - "latest-values": "آخرين مقادير", 1423 + "latest": "آخرين مقادير",
1424 "rpc": "ويجت کنترل", 1424 "rpc": "ويجت کنترل",
1425 "alarm": "ويجت هشدار", 1425 "alarm": "ويجت هشدار",
1426 "static": "ويجت ايستا", 1426 "static": "ويجت ايستا",
@@ -1494,7 +1494,7 @@ @@ -1494,7 +1494,7 @@
1494 "export": "Exporter widget", 1494 "export": "Exporter widget",
1495 "html": "HTML", 1495 "html": "HTML",
1496 "javascript": "Javascript", 1496 "javascript": "Javascript",
1497 - "latest-values": "Dernières valeurs", 1497 + "latest": "Dernières valeurs",
1498 "management": "Gestion des widgets", 1498 "management": "Gestion des widgets",
1499 "missing-widget-title-error": "Le titre du widget doit être spécifié!", 1499 "missing-widget-title-error": "Le titre du widget doit être spécifié!",
1500 "no-data-found": "Aucune donnée trouvée", 1500 "no-data-found": "Aucune donnée trouvée",
@@ -1463,7 +1463,7 @@ @@ -1463,7 +1463,7 @@
1463 "timeseries": "Time series", 1463 "timeseries": "Time series",
1464 "search-data": "Cerca dati", 1464 "search-data": "Cerca dati",
1465 "no-data-found": "Nessun dato trovato", 1465 "no-data-found": "Nessun dato trovato",
1466 - "latest-values": "Ultimi valori", 1466 + "latest": "Ultimi valori",
1467 "rpc": "Control widget", 1467 "rpc": "Control widget",
1468 "alarm": "Alarm widget", 1468 "alarm": "Alarm widget",
1469 "static": "Static widget", 1469 "static": "Static widget",
@@ -1305,7 +1305,7 @@ @@ -1305,7 +1305,7 @@
1305 "timeseries": "時系列", 1305 "timeseries": "時系列",
1306 "search-data": "検索データ", 1306 "search-data": "検索データ",
1307 "no-data-found": "何もデータが見つかりませんでした", 1307 "no-data-found": "何もデータが見つかりませんでした",
1308 - "latest-values": "最新の値", 1308 + "latest": "最新の値",
1309 "rpc": "コントロールウィジェット", 1309 "rpc": "コントロールウィジェット",
1310 "alarm": "アラームウィジェット", 1310 "alarm": "アラームウィジェット",
1311 "static": "静的ウィジェット", 1311 "static": "静的ウィジェット",
@@ -1551,7 +1551,7 @@ @@ -1551,7 +1551,7 @@
1551 "timeseries": "მონაცემთა სერია", 1551 "timeseries": "მონაცემთა სერია",
1552 "search-data": "საძიებო მონაცემები", 1552 "search-data": "საძიებო მონაცემები",
1553 "no-data-found": "მონაცემი ვერ მოიძებნა", 1553 "no-data-found": "მონაცემი ვერ მოიძებნა",
1554 - "latest-values": "უახლესი მნიშვნელობები", 1554 + "latest": "უახლესი მნიშვნელობები",
1555 "rpc": "ვიჯეტის კონტროლი", 1555 "rpc": "ვიჯეტის კონტროლი",
1556 "alarm": "ვიჯეტის განგაში", 1556 "alarm": "ვიჯეტის განგაში",
1557 "static": "სტატიკური ვიჯეტი", 1557 "static": "სტატიკური ვიჯეტი",
@@ -2202,7 +2202,7 @@ @@ -2202,7 +2202,7 @@
2202 "timeseries": "시계열", 2202 "timeseries": "시계열",
2203 "search-data": "데이터 검색", 2203 "search-data": "데이터 검색",
2204 "no-data-found": "아무 데이터도 없습니다", 2204 "no-data-found": "아무 데이터도 없습니다",
2205 - "latest-values": "최근 값", 2205 + "latest": "최근 값",
2206 "rpc": "컨트롤 위젯", 2206 "rpc": "컨트롤 위젯",
2207 "alarm": "알람 위젯", 2207 "alarm": "알람 위젯",
2208 "static": "상태 위젯", 2208 "static": "상태 위젯",
@@ -1468,7 +1468,7 @@ @@ -1468,7 +1468,7 @@
1468 "timeseries": "Laika sērijas", 1468 "timeseries": "Laika sērijas",
1469 "search-data": "Meklēt datus", 1469 "search-data": "Meklēt datus",
1470 "no-data-found": "Nav datu atrasti", 1470 "no-data-found": "Nav datu atrasti",
1471 - "latest-values": "Pedējās vērtības", 1471 + "latest": "Pedējās vērtības",
1472 "rpc": "Kontroles logrīks", 1472 "rpc": "Kontroles logrīks",
1473 "alarm": "Trauksmes logrīks", 1473 "alarm": "Trauksmes logrīks",
1474 "static": "Statiskais logrīks", 1474 "static": "Statiskais logrīks",
@@ -1777,7 +1777,7 @@ @@ -1777,7 +1777,7 @@
1777 "timeseries": "Intervalos de tempo", 1777 "timeseries": "Intervalos de tempo",
1778 "search-data": "Pesquisar dados", 1778 "search-data": "Pesquisar dados",
1779 "no-data-found": "Nenhum dado encontrado", 1779 "no-data-found": "Nenhum dado encontrado",
1780 - "latest-values": "Últimos valores", 1780 + "latest": "Últimos valores",
1781 "rpc": "Widget de controle", 1781 "rpc": "Widget de controle",
1782 "alarm": "Widget de alarme", 1782 "alarm": "Widget de alarme",
1783 "static": "Widget estático", 1783 "static": "Widget estático",
@@ -1535,7 +1535,7 @@ @@ -1535,7 +1535,7 @@
1535 "timeseries": "Serii Temporale", 1535 "timeseries": "Serii Temporale",
1536 "search-data": "Caută Date", 1536 "search-data": "Caută Date",
1537 "no-data-found": "Nu Au Fost Găsite Date", 1537 "no-data-found": "Nu Au Fost Găsite Date",
1538 - "latest-values": "Ultimele Valori", 1538 + "latest": "Ultimele Valori",
1539 "rpc": "Widget Control ", 1539 "rpc": "Widget Control ",
1540 "alarm": "Widget Alarmă", 1540 "alarm": "Widget Alarmă",
1541 "static": "Widget Static", 1541 "static": "Widget Static",
@@ -1541,7 +1541,7 @@ @@ -1541,7 +1541,7 @@
1541 "timeseries": "Телеметрия", 1541 "timeseries": "Телеметрия",
1542 "search-data": "Поиск данных", 1542 "search-data": "Поиск данных",
1543 "no-data-found": "Данные не найдено", 1543 "no-data-found": "Данные не найдено",
1544 - "latest-values": "Последние значения", 1544 + "latest": "Последние значения",
1545 "rpc": "Управляющий виджет", 1545 "rpc": "Управляющий виджет",
1546 "alarm": "Виджет оповещений", 1546 "alarm": "Виджет оповещений",
1547 "static": "Статический виджет", 1547 "static": "Статический виджет",
@@ -2202,7 +2202,7 @@ @@ -2202,7 +2202,7 @@
2202 "timeseries": "Časovne serije", 2202 "timeseries": "Časovne serije",
2203 "search-data": "Iskanje podatkov", 2203 "search-data": "Iskanje podatkov",
2204 "no-data-found": "Podatkov ni mogoče najti", 2204 "no-data-found": "Podatkov ni mogoče najti",
2205 - "latest-values": "Najnovejše vrednosti", 2205 + "latest": "Najnovejše vrednosti",
2206 "rpc": "Nadzorni pripomoček", 2206 "rpc": "Nadzorni pripomoček",
2207 "alarm": "Pripomoček za alarm", 2207 "alarm": "Pripomoček za alarm",
2208 "static": "Statični pripomoček", 2208 "static": "Statični pripomoček",
@@ -1464,7 +1464,7 @@ @@ -1464,7 +1464,7 @@
1464 "timeseries": "Zaman serisi", 1464 "timeseries": "Zaman serisi",
1465 "search-data": "Arama verileri", 1465 "search-data": "Arama verileri",
1466 "no-data-found": "Veri bulunamadı", 1466 "no-data-found": "Veri bulunamadı",
1467 - "latest-values": "Son değerler", 1467 + "latest": "Son değerler",
1468 "rpc": "Kontrol göstergesi", 1468 "rpc": "Kontrol göstergesi",
1469 "alarm": "Alarm göstergesi", 1469 "alarm": "Alarm göstergesi",
1470 "static": "Statik gösterge", 1470 "static": "Statik gösterge",
@@ -2111,7 +2111,7 @@ @@ -2111,7 +2111,7 @@
2111 "timeseries": "Телеметрія", 2111 "timeseries": "Телеметрія",
2112 "search-data": "Пошук даних", 2112 "search-data": "Пошук даних",
2113 "no-data-found": "Даних не знайдено", 2113 "no-data-found": "Даних не знайдено",
2114 - "latest-values": "Останні значення", 2114 + "latest": "Останні значення",
2115 "rpc": "Керуючий віджет", 2115 "rpc": "Керуючий віджет",
2116 "alarm": "Віджет сигнала тривоги", 2116 "alarm": "Віджет сигнала тривоги",
2117 "static": "Статичний віджет", 2117 "static": "Статичний віджет",
@@ -2323,7 +2323,7 @@ @@ -2323,7 +2323,7 @@
2323 "html": "HTML", 2323 "html": "HTML",
2324 "javascript": "Javascript", 2324 "javascript": "Javascript",
2325 "js": "JS", 2325 "js": "JS",
2326 - "latest-values": "最新值", 2326 + "latest": "最新值",
2327 "management": "管理部件", 2327 "management": "管理部件",
2328 "missing-widget-title-error": "部件标题必须指定!", 2328 "missing-widget-title-error": "部件标题必须指定!",
2329 "no-data": "小部件上没有要显示的数据", 2329 "no-data": "小部件上没有要显示的数据",
@@ -2500,4 +2500,4 @@ @@ -2500,4 +2500,4 @@
2500 "value": "价值" 2500 "value": "价值"
2501 } 2501 }
2502 } 2502 }
2503 -}  
  2503 +}
@@ -1396,7 +1396,7 @@ @@ -1396,7 +1396,7 @@
1396 "timeseries": "時間序列", 1396 "timeseries": "時間序列",
1397 "search-data": "搜尋資料", 1397 "search-data": "搜尋資料",
1398 "no-data-found": "沒有找到資料", 1398 "no-data-found": "沒有找到資料",
1399 - "latest-values": "最新值", 1399 + "latest": "最新值",
1400 "rpc": "控件部件", 1400 "rpc": "控件部件",
1401 "alarm": "警告部件", 1401 "alarm": "警告部件",
1402 "static": "靜態部件", 1402 "static": "靜態部件",