Commit 22a1d24291d8e0a794f45935666f1c6fe3710676
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 | } | ... | ... |
... | ... | @@ -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 { | ... | ... |
ui-ngx/src/app/shared/pipe/file-size.pipe.ts
0 → 100644
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 | +} | ... | ... |
... | ... | @@ -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": "სტატიკური ვიჯეტი", | ... | ... |
... | ... | @@ -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 | +} | ... | ... |