Commit 65b7c139cf31cfc6dd78af08ca509924d00d5579

Authored by Igor Kulikov
1 parent 8ee3f0bf

Widgets library import export support.

@@ -55,7 +55,7 @@ @@ -55,7 +55,7 @@
55 ], 55 ],
56 "templateHtml": "<canvas id=\"pieChart\"></canvas>\n", 56 "templateHtml": "<canvas id=\"pieChart\"></canvas>\n",
57 "templateCss": "", 57 "templateCss": "",
58 - "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = angular.isDefined(self.ctx.settings.borderWidth) ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n", 58 + "controllerScript": "self.onInit = function() {\n var pieData = {\n labels: [],\n datasets: []\n };\n\n var dataset = {\n data: [],\n backgroundColor: [],\n borderColor: [],\n borderWidth: [],\n hoverBackgroundColor: []\n }\n \n var borderColor = self.ctx.settings.borderColor || '#fff';\n var borderWidth = typeof self.ctx.settings.borderWidth !== 'undefined' ? self.ctx.settings.borderWidth : 5;\n \n pieData.datasets.push(dataset);\n \n for (var i=0; i < self.ctx.data.length; i++) {\n var dataKey = self.ctx.data[i].dataKey;\n pieData.labels.push(dataKey.label);\n dataset.data.push(0);\n var hoverBackgroundColor = tinycolor(dataKey.color).lighten(15);\n dataset.backgroundColor.push(dataKey.color);\n dataset.borderColor.push(borderColor);\n dataset.borderWidth.push(borderWidth);\n dataset.hoverBackgroundColor.push(hoverBackgroundColor.toRgbString());\n }\n\n var options = {\n responsive: false,\n maintainAspectRatio: false,\n legend: {\n display: true,\n labels: {\n fontColor: '#666'\n }\n },\n tooltips: {\n callbacks: {\n label: function(tooltipItem, data) {\n var label = data.labels[tooltipItem.index];\n var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];\n var content = label + ': ' + value;\n var units = self.ctx.settings.units ? self.ctx.settings.units : self.ctx.units;\n if (units) {\n content += ' ' + units;\n } \n return content;\n }\n }\n }\n };\n\n if (self.ctx.settings.legend) {\n options.legend.display = self.ctx.settings.legend.display !== false;\n options.legend.labels.fontColor = self.ctx.settings.legend.labelsFontColor || '#666';\n }\n\n var ctx = $('#pieChart', self.ctx.$container);\n self.ctx.chart = new Chart(ctx, {\n type: 'doughnut',\n data: pieData,\n options: options\n });\n \n self.onResize();\n}\n\nself.onDataUpdated = function() {\n for (var i = 0; i < self.ctx.data.length; i++) {\n var cellData = self.ctx.data[i];\n if (cellData.data.length > 0) {\n var tvPair = cellData.data[cellData.data.length - 1];\n var value = tvPair[1];\n self.ctx.chart.data.datasets[0].data[i] = parseFloat(value);\n }\n }\n self.ctx.chart.update();\n}\n\nself.onResize = function() {\n self.ctx.chart.resize();\n}\n\n",
59 "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"borderWidth\": {\n \"title\": \"Border width\",\n \"type\": \"number\",\n \"default\": 5\n },\n \"borderColor\": {\n \"title\": \"Border color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"legend\": {\n \"title\": \"Legend settings\",\n \"type\": \"object\",\n \"properties\": {\n \"display\": {\n \"title\": \"Display legend\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelsFontColor\": {\n \"title\": \"Labels font color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n }\n }\n }\n },\n \"required\": []\n },\n \"form\": [\n \"borderWidth\", \n {\n \"key\": \"borderColor\",\n \"type\": \"color\"\n }, \n {\n \"key\": \"legend\",\n \"items\": [\n \"legend.display\",\n {\n \"key\": \"legend.labelsFontColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}", 59 "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"properties\": {\n \"borderWidth\": {\n \"title\": \"Border width\",\n \"type\": \"number\",\n \"default\": 5\n },\n \"borderColor\": {\n \"title\": \"Border color\",\n \"type\": \"string\",\n \"default\": \"#fff\"\n },\n \"legend\": {\n \"title\": \"Legend settings\",\n \"type\": \"object\",\n \"properties\": {\n \"display\": {\n \"title\": \"Display legend\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"labelsFontColor\": {\n \"title\": \"Labels font color\",\n \"type\": \"string\",\n \"default\": \"#666\"\n }\n }\n }\n },\n \"required\": []\n },\n \"form\": [\n \"borderWidth\", \n {\n \"key\": \"borderColor\",\n \"type\": \"color\"\n }, \n {\n \"key\": \"legend\",\n \"items\": [\n \"legend.display\",\n {\n \"key\": \"legend.labelsFontColor\",\n \"type\": \"color\"\n }\n ]\n }\n ]\n}",
60 "dataKeySettingsSchema": "{}\n", 60 "dataKeySettingsSchema": "{}\n",
61 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"borderWidth\":5,\"borderColor\":\"#fff\",\"legend\":{\"display\":true,\"labelsFontColor\":\"#666666\"}},\"title\":\"Doughnut - Chart.js\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}" 61 "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"First\",\"color\":\"#26a69a\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Second\",\"color\":\"#f57c00\",\"settings\":{},\"_hash\":0.545701115289893,\"funcBody\":\"var value = (prevValue-20) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+20;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Third\",\"color\":\"#afb42b\",\"settings\":{},\"_hash\":0.2592906835158064,\"funcBody\":\"var value = (prevValue-40) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+40;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Fourth\",\"color\":\"#673ab7\",\"settings\":{},\"_hash\":0.12880275585455747,\"funcBody\":\"var value = (prevValue-50) + Math.random() * 2 - 1;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value+50;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"borderWidth\":5,\"borderColor\":\"#fff\",\"legend\":{\"display\":true,\"labelsFontColor\":\"#666666\"}},\"title\":\"Doughnut - Chart.js\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400}}"
@@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http'; @@ -21,7 +21,7 @@ import { HttpClient } from '@angular/common/http';
21 import { PageLink } from '@shared/models/page/page-link'; 21 import { PageLink } from '@shared/models/page/page-link';
22 import { PageData } from '@shared/models/page/page-data'; 22 import { PageData } from '@shared/models/page/page-data';
23 import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; 23 import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
24 -import { WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared/models/widget.models'; 24 +import { Widget, WidgetType, widgetType, WidgetTypeData, widgetTypesData } from '@shared/models/widget.models';
25 import { UtilsService } from '@core/services/utils.service'; 25 import { UtilsService } from '@core/services/utils.service';
26 import { TranslateService } from '@ngx-translate/core'; 26 import { TranslateService } from '@ngx-translate/core';
27 import { ResourcesService } from '../services/resources.service'; 27 import { ResourcesService } from '../services/resources.service';
@@ -119,6 +119,59 @@ export class WidgetService { @@ -119,6 +119,59 @@ export class WidgetService {
119 defaultHttpOptions(ignoreLoading, ignoreErrors)); 119 defaultHttpOptions(ignoreLoading, ignoreErrors));
120 } 120 }
121 121
  122 + public loadBundleLibraryWidgets(bundleAlias: string, isSystem: boolean,
  123 + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<Array<Widget>> {
  124 + return this.getBundleWidgetTypes(bundleAlias, isSystem, ignoreErrors, ignoreLoading).pipe(
  125 + map((types) => {
  126 + types = types.sort((a, b) => {
  127 + let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);
  128 + if (result === 0) {
  129 + result = b.createdTime - a.createdTime;
  130 + }
  131 + return result;
  132 + });
  133 + const widgetTypes = new Array<Widget>();
  134 + let top = 0;
  135 + const lastTop = [0, 0, 0];
  136 + let col = 0;
  137 + let column = 0;
  138 + types.forEach((type) => {
  139 + const widgetTypeInfo = toWidgetInfo(type);
  140 + const sizeX = 8;
  141 + const sizeY = Math.floor(widgetTypeInfo.sizeY);
  142 + const widget: Widget = {
  143 + typeId: type.id,
  144 + isSystemType: isSystem,
  145 + bundleAlias,
  146 + typeAlias: widgetTypeInfo.alias,
  147 + type: widgetTypeInfo.type,
  148 + title: widgetTypeInfo.widgetName,
  149 + sizeX,
  150 + sizeY,
  151 + row: top,
  152 + col,
  153 + config: JSON.parse(widgetTypeInfo.defaultConfig)
  154 + };
  155 +
  156 + widget.config.title = widgetTypeInfo.widgetName;
  157 +
  158 + widgetTypes.push(widget);
  159 + top += sizeY;
  160 + if (top > lastTop[column] + 10) {
  161 + lastTop[column] = top;
  162 + column++;
  163 + if (column > 2) {
  164 + column = 0;
  165 + }
  166 + top = lastTop[column];
  167 + col = column * 8;
  168 + }
  169 + });
  170 + return widgetTypes;
  171 + })
  172 + );
  173 + }
  174 +
122 public getWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean, 175 public getWidgetType(bundleAlias: string, widgetTypeAlias: string, isSystem: boolean,
123 ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> { 176 ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable<WidgetType> {
124 return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`, 177 return this.http.get<WidgetType>(`/api/widgetType?isSystem=${isSystem}&bundleAlias=${bundleAlias}&alias=${widgetTypeAlias}`,
@@ -14,10 +14,16 @@ @@ -14,10 +14,16 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Widget } from '@app/shared/models/widget.models'; 17 +import { Widget, WidgetType } from '@app/shared/models/widget.models';
18 import { DashboardLayoutId } from '@shared/models/dashboard.models'; 18 import { DashboardLayoutId } from '@shared/models/dashboard.models';
  19 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
19 20
20 export interface ImportWidgetResult { 21 export interface ImportWidgetResult {
21 widget: Widget; 22 widget: Widget;
22 layoutId: DashboardLayoutId; 23 layoutId: DashboardLayoutId;
23 } 24 }
  25 +
  26 +export interface WidgetsBundleItem {
  27 + widgetsBundle: WidgetsBundle;
  28 + widgetTypes: WidgetType[];
  29 +}
@@ -38,15 +38,18 @@ import { forkJoin, Observable, of } from 'rxjs'; @@ -38,15 +38,18 @@ import { forkJoin, Observable, of } from 'rxjs';
38 import { catchError, map, mergeMap } from 'rxjs/operators'; 38 import { catchError, map, mergeMap } from 'rxjs/operators';
39 import { DashboardUtilsService } from '@core/services/dashboard-utils.service'; 39 import { DashboardUtilsService } from '@core/services/dashboard-utils.service';
40 import { EntityService } from '@core/http/entity.service'; 40 import { EntityService } from '@core/http/entity.service';
41 -import { Widget, WidgetSize } from '@shared/models/widget.models'; 41 +import { Widget, WidgetSize, WidgetType } from '@shared/models/widget.models';
42 import { 42 import {
43 EntityAliasesDialogComponent, 43 EntityAliasesDialogComponent,
44 EntityAliasesDialogData 44 EntityAliasesDialogData
45 } from '@home/components/alias/entity-aliases-dialog.component'; 45 } from '@home/components/alias/entity-aliases-dialog.component';
46 import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service'; 46 import { ItemBufferService, WidgetItem } from '@core/services/item-buffer.service';
47 -import { ImportWidgetResult } from './import-export.models'; 47 +import { ImportWidgetResult, WidgetsBundleItem } from './import-export.models';
48 import { EntityType } from '@shared/models/entity-type.models'; 48 import { EntityType } from '@shared/models/entity-type.models';
49 import { UtilsService } from '@core/services/utils.service'; 49 import { UtilsService } from '@core/services/utils.service';
  50 +import { WidgetService } from '@core/http/widget.service';
  51 +import { NULL_UUID } from '@shared/models/id/has-uuid';
  52 +import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
50 53
51 @Injectable() 54 @Injectable()
52 export class ImportExportService { 55 export class ImportExportService {
@@ -57,6 +60,7 @@ export class ImportExportService { @@ -57,6 +60,7 @@ export class ImportExportService {
57 private translate: TranslateService, 60 private translate: TranslateService,
58 private dashboardService: DashboardService, 61 private dashboardService: DashboardService,
59 private dashboardUtils: DashboardUtilsService, 62 private dashboardUtils: DashboardUtilsService,
  63 + private widgetService: WidgetService,
60 private entityService: EntityService, 64 private entityService: EntityService,
61 private utils: UtilsService, 65 private utils: UtilsService,
62 private itembuffer: ItemBufferService, 66 private itembuffer: ItemBufferService,
@@ -72,13 +76,7 @@ export class ImportExportService { @@ -72,13 +76,7 @@ export class ImportExportService {
72 this.exportToPc(this.prepareDashboardExport(dashboard), name + '.json'); 76 this.exportToPc(this.prepareDashboardExport(dashboard), name + '.json');
73 }, 77 },
74 (e) => { 78 (e) => {
75 - let message = e;  
76 - if (!message) {  
77 - message = this.translate.instant('error.unknown-error');  
78 - }  
79 - this.store.dispatch(new ActionNotificationShow(  
80 - {message: this.translate.instant('dashboard.export-failed-error', {error: message}),  
81 - type: 'error'})); 79 + this.handleExportError(e, 'dashboard.export-failed-error');
82 } 80 }
83 ); 81 );
84 } 82 }
@@ -223,6 +221,119 @@ export class ImportExportService { @@ -223,6 +221,119 @@ export class ImportExportService {
223 ); 221 );
224 } 222 }
225 223
  224 + public exportWidgetType(widgetTypeId: string) {
  225 + this.widgetService.getWidgetTypeById(widgetTypeId).subscribe(
  226 + (widgetType) => {
  227 + if (isDefined(widgetType.bundleAlias)) {
  228 + delete widgetType.bundleAlias;
  229 + }
  230 + let name = widgetType.name;
  231 + name = name.toLowerCase().replace(/\W/g, '_');
  232 + this.exportToPc(this.prepareExport(widgetType), name + '.json');
  233 + },
  234 + (e) => {
  235 + this.handleExportError(e, 'widget-type.export-failed-error');
  236 + }
  237 + );
  238 + }
  239 +
  240 + public importWidgetType(bundleAlias: string): Observable<WidgetType> {
  241 + return this.openImportDialog('widget-type.import', 'widget-type.widget-type-file').pipe(
  242 + mergeMap((widgetType: WidgetType) => {
  243 + if (!this.validateImportedWidgetType(widgetType)) {
  244 + this.store.dispatch(new ActionNotificationShow(
  245 + {message: this.translate.instant('widget-type.invalid-widget-type-file-error'),
  246 + type: 'error'}));
  247 + throw new Error('Invalid widget type file');
  248 + } else {
  249 + widgetType.bundleAlias = bundleAlias;
  250 + return this.widgetService.saveImportedWidgetType(widgetType);
  251 + }
  252 + }),
  253 + catchError((err) => {
  254 + return of(null);
  255 + })
  256 + );
  257 + }
  258 +
  259 + public exportWidgetsBundle(widgetsBundleId: string) {
  260 + this.widgetService.getWidgetsBundle(widgetsBundleId).subscribe(
  261 + (widgetsBundle) => {
  262 + const bundleAlias = widgetsBundle.alias;
  263 + const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
  264 + this.widgetService.getBundleWidgetTypes(bundleAlias, isSystem).subscribe(
  265 + (widgetTypes) => {
  266 + const widgetsBundleItem: WidgetsBundleItem = {
  267 + widgetsBundle: this.prepareExport(widgetsBundle),
  268 + widgetTypes: []
  269 + };
  270 + for (const widgetType of widgetTypes) {
  271 + if (isDefined(widgetType.bundleAlias)) {
  272 + delete widgetType.bundleAlias;
  273 + }
  274 + widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetType));
  275 + }
  276 + let name = widgetsBundle.title;
  277 + name = name.toLowerCase().replace(/\W/g, '_');
  278 + this.exportToPc(widgetsBundleItem, name + '.json');
  279 + },
  280 + (e) => {
  281 + this.handleExportError(e, 'widgets-bundle.export-failed-error');
  282 + }
  283 + );
  284 + },
  285 + (e) => {
  286 + this.handleExportError(e, 'widgets-bundle.export-failed-error');
  287 + }
  288 + );
  289 + }
  290 +
  291 + public importWidgetsBundle(): Observable<WidgetsBundle> {
  292 + return this.openImportDialog('widgets-bundle.import', 'widgets-bundle.widgets-bundle-file').pipe(
  293 + mergeMap((widgetsBundleItem: WidgetsBundleItem) => {
  294 + if (!this.validateImportedWidgetsBundle(widgetsBundleItem)) {
  295 + this.store.dispatch(new ActionNotificationShow(
  296 + {message: this.translate.instant('widgets-bundle.invalid-widgets-bundle-file-error'),
  297 + type: 'error'}));
  298 + throw new Error('Invalid widgets bundle file');
  299 + } else {
  300 + const widgetsBundle = widgetsBundleItem.widgetsBundle;
  301 + return this.widgetService.saveWidgetsBundle(widgetsBundle).pipe(
  302 + mergeMap((savedWidgetsBundle) => {
  303 + const bundleAlias = savedWidgetsBundle.alias;
  304 + const widgetTypes = widgetsBundleItem.widgetTypes;
  305 + if (widgetTypes.length) {
  306 + const saveWidgetTypesObservables: Array<Observable<WidgetType>> = [];
  307 + for (const widgetType of widgetTypes) {
  308 + widgetType.bundleAlias = bundleAlias;
  309 + saveWidgetTypesObservables.push(this.widgetService.saveImportedWidgetType(widgetType));
  310 + }
  311 + return forkJoin(saveWidgetTypesObservables).pipe(
  312 + map(() => savedWidgetsBundle)
  313 + );
  314 + } else {
  315 + return of(savedWidgetsBundle);
  316 + }
  317 + }
  318 + ));
  319 + }
  320 + }),
  321 + catchError((err) => {
  322 + return of(null);
  323 + })
  324 + );
  325 + }
  326 +
  327 + private handleExportError(e: any, errorDetailsMessageId: string) {
  328 + let message = e;
  329 + if (!message) {
  330 + message = this.translate.instant('error.unknown-error');
  331 + }
  332 + this.store.dispatch(new ActionNotificationShow(
  333 + {message: this.translate.instant(errorDetailsMessageId, {error: message}),
  334 + type: 'error'}));
  335 + }
  336 +
226 private validateImportedDashboard(dashboard: Dashboard): boolean { 337 private validateImportedDashboard(dashboard: Dashboard): boolean {
227 if (isUndefined(dashboard.title) || isUndefined(dashboard.configuration)) { 338 if (isUndefined(dashboard.title) || isUndefined(dashboard.configuration)) {
228 return false; 339 return false;
@@ -246,6 +357,34 @@ export class ImportExportService { @@ -246,6 +357,34 @@ export class ImportExportService {
246 return true; 357 return true;
247 } 358 }
248 359
  360 + private validateImportedWidgetType(widgetType: WidgetType): boolean {
  361 + if (isUndefined(widgetType.name)
  362 + || isUndefined(widgetType.descriptor)) {
  363 + return false;
  364 + }
  365 + return true;
  366 + }
  367 +
  368 + private validateImportedWidgetsBundle(widgetsBundleItem: WidgetsBundleItem): boolean {
  369 + if (isUndefined(widgetsBundleItem.widgetsBundle)) {
  370 + return false;
  371 + }
  372 + if (isUndefined(widgetsBundleItem.widgetTypes)) {
  373 + return false;
  374 + }
  375 + const widgetsBundle = widgetsBundleItem.widgetsBundle;
  376 + if (isUndefined(widgetsBundle.title)) {
  377 + return false;
  378 + }
  379 + const widgetTypes = widgetsBundleItem.widgetTypes;
  380 + for (const widgetType of widgetTypes) {
  381 + if (!this.validateImportedWidgetType(widgetType)) {
  382 + return false;
  383 + }
  384 + }
  385 + return true;
  386 + }
  387 +
249 private saveImportedDashboard(dashboard: Dashboard): Observable<Dashboard> { 388 private saveImportedDashboard(dashboard: Dashboard): Observable<Dashboard> {
250 return this.dashboardService.saveDashboard(dashboard); 389 return this.dashboardService.saveDashboard(dashboard);
251 } 390 }
@@ -114,7 +114,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> { @@ -114,7 +114,7 @@ export class DashboardWidgets implements Iterable<DashboardWidget> {
114 updateRecords.push({ 114 updateRecords.push({
115 widget: added.item, 115 widget: added.item,
116 widgetId: added.item.id, 116 widgetId: added.item.id,
117 - widgetLayout: this.widgetLayouts[added.item.id], 117 + widgetLayout: this.widgetLayouts ? this.widgetLayouts[added.item.id] : null,
118 operation: 'add' 118 operation: 'add'
119 }); 119 });
120 }); 120 });
@@ -63,54 +63,10 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> { @@ -63,54 +63,10 @@ export class WidgetsTypesDataResolver implements Resolve<WidgetsData> {
63 const widgetsBundle: WidgetsBundle = route.parent.data.widgetsBundle; 63 const widgetsBundle: WidgetsBundle = route.parent.data.widgetsBundle;
64 const bundleAlias = widgetsBundle.alias; 64 const bundleAlias = widgetsBundle.alias;
65 const isSystem = widgetsBundle.tenantId.id === NULL_UUID; 65 const isSystem = widgetsBundle.tenantId.id === NULL_UUID;
66 - return this.widgetsService.getBundleWidgetTypes(bundleAlias, 66 + return this.widgetsService.loadBundleLibraryWidgets(bundleAlias,
67 isSystem).pipe( 67 isSystem).pipe(
68 - map((types) => {  
69 - types = types.sort((a, b) => {  
70 - let result = widgetType[b.descriptor.type].localeCompare(widgetType[a.descriptor.type]);  
71 - if (result === 0) {  
72 - result = b.createdTime - a.createdTime;  
73 - }  
74 - return result;  
75 - });  
76 - const widgetTypes = new Array<Widget>();  
77 - let top = 0;  
78 - const lastTop = [0, 0, 0];  
79 - let col = 0;  
80 - let column = 0;  
81 - types.forEach((type) => {  
82 - const widgetTypeInfo = toWidgetInfo(type);  
83 - const sizeX = 8;  
84 - const sizeY = Math.floor(widgetTypeInfo.sizeY);  
85 - const widget: Widget = {  
86 - typeId: type.id,  
87 - isSystemType: isSystem,  
88 - bundleAlias,  
89 - typeAlias: widgetTypeInfo.alias,  
90 - type: widgetTypeInfo.type,  
91 - title: widgetTypeInfo.widgetName,  
92 - sizeX,  
93 - sizeY,  
94 - row: top,  
95 - col,  
96 - config: JSON.parse(widgetTypeInfo.defaultConfig)  
97 - };  
98 -  
99 - widget.config.title = widgetTypeInfo.widgetName;  
100 -  
101 - widgetTypes.push(widget);  
102 - top += sizeY;  
103 - if (top > lastTop[column] + 10) {  
104 - lastTop[column] = top;  
105 - column++;  
106 - if (column > 2) {  
107 - column = 0;  
108 - }  
109 - top = lastTop[column];  
110 - col = column * 8;  
111 - }  
112 - });  
113 - return { widgets: widgetTypes }; 68 + map((widgets) => {
  69 + return { widgets };
114 } 70 }
115 )); 71 ));
116 } 72 }
@@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -import { Component, OnInit, ViewChild } from '@angular/core'; 17 +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
18 import { Store } from '@ngrx/store'; 18 import { Store } from '@ngrx/store';
19 import { AppState } from '@core/core.state'; 19 import { AppState } from '@core/core.state';
20 import { PageComponent } from '@shared/components/page.component'; 20 import { PageComponent } from '@shared/components/page.component';
@@ -24,7 +24,7 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model'; @@ -24,7 +24,7 @@ import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
24 import { ActivatedRoute, Router } from '@angular/router'; 24 import { ActivatedRoute, Router } from '@angular/router';
25 import { Authority } from '@shared/models/authority.enum'; 25 import { Authority } from '@shared/models/authority.enum';
26 import { NULL_UUID } from '@shared/models/id/has-uuid'; 26 import { NULL_UUID } from '@shared/models/id/has-uuid';
27 -import { Observable, of } from 'rxjs'; 27 +import { Observable, of, Subscription } from 'rxjs';
28 import { Widget, widgetType } from '@app/shared/models/widget.models'; 28 import { Widget, widgetType } from '@app/shared/models/widget.models';
29 import { WidgetService } from '@core/http/widget.service'; 29 import { WidgetService } from '@core/http/widget.service';
30 import { map, mergeMap, share } from 'rxjs/operators'; 30 import { map, mergeMap, share } from 'rxjs/operators';
@@ -42,6 +42,7 @@ import { DeviceCredentials } from '@shared/models/device.models'; @@ -42,6 +42,7 @@ import { DeviceCredentials } from '@shared/models/device.models';
42 import { MatDialog } from '@angular/material/dialog'; 42 import { MatDialog } from '@angular/material/dialog';
43 import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component'; 43 import { SelectWidgetTypeDialogComponent } from '@home/pages/widget/select-widget-type-dialog.component';
44 import { TranslateService } from '@ngx-translate/core'; 44 import { TranslateService } from '@ngx-translate/core';
  45 +import { ImportExportService } from '@home/components/import-export/import-export.service';
45 46
46 @Component({ 47 @Component({
47 selector: 'tb-widget-library', 48 selector: 'tb-widget-library',
@@ -94,11 +95,12 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -94,11 +95,12 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
94 private router: Router, 95 private router: Router,
95 private widgetService: WidgetService, 96 private widgetService: WidgetService,
96 private dialogService: DialogService, 97 private dialogService: DialogService,
  98 + private importExport: ImportExportService,
97 private dialog: MatDialog, 99 private dialog: MatDialog,
98 private translate: TranslateService) { 100 private translate: TranslateService) {
99 super(store); 101 super(store);
100 102
101 - this.authUser = getCurrentAuthUser(store); 103 + this.authUser = getCurrentAuthUser(this.store);
102 this.widgetsBundle = this.route.snapshot.data.widgetsBundle; 104 this.widgetsBundle = this.route.snapshot.data.widgetsBundle;
103 this.widgetsData = this.route.snapshot.data.widgetsData; 105 this.widgetsData = this.route.snapshot.data.widgetsData;
104 if (this.authUser.authority === Authority.TENANT_ADMIN) { 106 if (this.authUser.authority === Authority.TENANT_ADMIN) {
@@ -119,7 +121,23 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -119,7 +121,23 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
119 if ($event) { 121 if ($event) {
120 $event.stopPropagation(); 122 $event.stopPropagation();
121 } 123 }
122 - this.dialogService.todo(); 124 + this.importExport.importWidgetType(this.widgetsBundle.alias).subscribe(
  125 + (widgetTypeInstance) => {
  126 + if (widgetTypeInstance) {
  127 + this.reload();
  128 + }
  129 + }
  130 + );
  131 + }
  132 +
  133 + private reload() {
  134 + const bundleAlias = this.widgetsBundle.alias;
  135 + const isSystem = this.widgetsBundle.tenantId.id === NULL_UUID;
  136 + this.widgetService.loadBundleLibraryWidgets(bundleAlias, isSystem).subscribe(
  137 + (widgets) => {
  138 + this.widgetsData = {widgets};
  139 + }
  140 + );
123 } 141 }
124 142
125 openWidgetType($event: Event, widget?: Widget): void { 143 openWidgetType($event: Event, widget?: Widget): void {
@@ -147,14 +165,14 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -147,14 +165,14 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
147 if ($event) { 165 if ($event) {
148 $event.stopPropagation(); 166 $event.stopPropagation();
149 } 167 }
150 - this.dialogService.todo(); 168 + this.importExport.exportWidgetType(widget.typeId.id);
151 } 169 }
152 170
153 - removeWidgetType($event: Event, widget: Widget): Observable<boolean> { 171 + removeWidgetType($event: Event, widget: Widget): void {
154 if ($event) { 172 if ($event) {
155 $event.stopPropagation(); 173 $event.stopPropagation();
156 } 174 }
157 - return this.dialogService.confirm( 175 + this.dialogService.confirm(
158 this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}), 176 this.translate.instant('widget.remove-widget-type-title', {widgetName: widget.config.title}),
159 this.translate.instant('widget.remove-widget-type-text'), 177 this.translate.instant('widget.remove-widget-type-text'),
160 this.translate.instant('action.no'), 178 this.translate.instant('action.no'),
@@ -169,13 +187,13 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit { @@ -169,13 +187,13 @@ export class WidgetLibraryComponent extends PageComponent implements OnInit {
169 }), 187 }),
170 map((result) => { 188 map((result) => {
171 if (result !== false) { 189 if (result !== false) {
172 - this.widgetsData.widgets.splice(this.widgetsData.widgets.indexOf(widget), 1); 190 + this.reload();
173 return true; 191 return true;
174 } else { 192 } else {
175 return false; 193 return false;
176 } 194 }
177 } 195 }
178 - )); 196 + )).subscribe();
179 } 197 }
180 198
181 } 199 }
@@ -36,6 +36,7 @@ import {AppState} from '@core/core.state'; @@ -36,6 +36,7 @@ import {AppState} from '@core/core.state';
36 import {getCurrentAuthUser} from '@app/core/auth/auth.selectors'; 36 import {getCurrentAuthUser} from '@app/core/auth/auth.selectors';
37 import {Authority} from '@shared/models/authority.enum'; 37 import {Authority} from '@shared/models/authority.enum';
38 import {DialogService} from '@core/services/dialog.service'; 38 import {DialogService} from '@core/services/dialog.service';
  39 +import { ImportExportService } from '@home/components/import-export/import-export.service';
39 40
40 @Injectable() 41 @Injectable()
41 export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableConfig<WidgetsBundle>> { 42 export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableConfig<WidgetsBundle>> {
@@ -46,6 +47,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon @@ -46,6 +47,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
46 private dialogService: DialogService, 47 private dialogService: DialogService,
47 private widgetsService: WidgetService, 48 private widgetsService: WidgetService,
48 private translate: TranslateService, 49 private translate: TranslateService,
  50 + private importExport: ImportExportService,
49 private datePipe: DatePipe, 51 private datePipe: DatePipe,
50 private router: Router) { 52 private router: Router) {
51 53
@@ -124,11 +126,13 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon @@ -124,11 +126,13 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
124 } 126 }
125 127
126 importWidgetsBundle($event: Event) { 128 importWidgetsBundle($event: Event) {
127 - if ($event) {  
128 - $event.stopPropagation();  
129 - }  
130 - // TODO:  
131 - this.dialogService.todo(); 129 + this.importExport.importWidgetsBundle().subscribe(
  130 + (widgetsBundle) => {
  131 + if (widgetsBundle) {
  132 + this.config.table.updateData();
  133 + }
  134 + }
  135 + );
132 } 136 }
133 137
134 openWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) { 138 openWidgetsBundle($event: Event, widgetsBundle: WidgetsBundle) {
@@ -142,8 +146,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon @@ -142,8 +146,7 @@ export class WidgetsBundlesTableConfigResolver implements Resolve<EntityTableCon
142 if ($event) { 146 if ($event) {
143 $event.stopPropagation(); 147 $event.stopPropagation();
144 } 148 }
145 - // TODO:  
146 - this.dialogService.todo(); 149 + this.importExport.exportWidgetsBundle(widgetsBundle.id.id);
147 } 150 }
148 151
149 onWidgetsBundleAction(action: EntityAction<WidgetsBundle>): boolean { 152 onWidgetsBundleAction(action: EntityAction<WidgetsBundle>): boolean {
@@ -21,12 +21,12 @@ @@ -21,12 +21,12 @@
21 min-height: #{$size}px; 21 min-height: #{$size}px;
22 font-size: #{$size}px; 22 font-size: #{$size}px;
23 line-height: #{$size}px; 23 line-height: #{$size}px;
24 - svg { 24 +/* svg {
25 width: 24px; 25 width: 24px;
26 height: 24px; 26 height: 24px;
27 transform: scale($size/24); 27 transform: scale($size/24);
28 transform-origin: top; 28 transform-origin: top;
29 - } 29 + }*/
30 } 30 }
31 31
32 @mixin tb-mat-icon-button-size($size) { 32 @mixin tb-mat-icon-button-size($size) {