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