Commit 1305015585c5bc4581d22a7dfbbc7fdb4c912b0e

Authored by Andrew Shvayka
Committed by GitHub
2 parents f78b8e55 9c616bd3

Merge pull request #33 from thingsboard/feature/import-export

Import/Export. Bug fixes.
... ... @@ -54,6 +54,7 @@
54 54 "json-schema-defaults": "^0.2.0",
55 55 "justgage": "^1.2.2",
56 56 "material-ui": "^0.16.1",
  57 + "material-ui-number-input": "^5.0.16",
57 58 "md-color-picker": "^0.2.6",
58 59 "mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5",
59 60 "moment": "^2.15.0",
... ...
... ... @@ -88,10 +88,10 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
88 88 return deferred.promise;
89 89 }
90 90
91   - function getDevice(deviceId) {
  91 + function getDevice(deviceId, ignoreErrors) {
92 92 var deferred = $q.defer();
93 93 var url = '/api/device/' + deviceId;
94   - $http.get(url, null).then(function success(response) {
  94 + $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) {
95 95 deferred.resolve(response.data);
96 96 }, function fail(response) {
97 97 deferred.reject(response.data);
... ...
... ... @@ -58,8 +58,10 @@ function Dashboard() {
58 58 isMobile: '=',
59 59 isMobileDisabled: '=?',
60 60 isEditActionEnabled: '=',
  61 + isExportActionEnabled: '=',
61 62 isRemoveActionEnabled: '=',
62 63 onEditWidget: '&?',
  64 + onExportWidget: '&?',
63 65 onRemoveWidget: '&?',
64 66 onWidgetMouseDown: '&?',
65 67 onWidgetClicked: '&?',
... ... @@ -139,6 +141,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
139 141 vm.showWidgetTitle = showWidgetTitle;
140 142 vm.hasTimewindow = hasTimewindow;
141 143 vm.editWidget = editWidget;
  144 + vm.exportWidget = exportWidget;
142 145 vm.removeWidget = removeWidget;
143 146 vm.loading = loading;
144 147
... ... @@ -413,6 +416,16 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
413 416 }
414 417 }
415 418
  419 + function exportWidget ($event, widget) {
  420 + resetWidgetClick();
  421 + if ($event) {
  422 + $event.stopPropagation();
  423 + }
  424 + if (vm.isExportActionEnabled && vm.onExportWidget) {
  425 + vm.onExportWidget({event: $event, widget: widget});
  426 + }
  427 + }
  428 +
416 429 function removeWidget($event, widget) {
417 430 resetWidgetClick();
418 431 if ($event) {
... ...
... ... @@ -62,6 +62,18 @@
62 62 edit
63 63 </md-icon>
64 64 </md-button>
  65 + <md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded"
  66 + ng-disabled="vm.loading()"
  67 + class="md-icon-button md-primary"
  68 + ng-click="vm.exportWidget($event, widget)"
  69 + aria-label="{{ 'widget.export' | translate }}">
  70 + <md-tooltip md-direction="top">
  71 + {{ 'widget.export' | translate }}
  72 + </md-tooltip>
  73 + <md-icon class="material-icons">
  74 + file_download
  75 + </md-icon>
  76 + </md-button>
65 77 <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
66 78 ng-disabled="vm.loading()"
67 79 class="md-icon-button md-primary"
... ...
... ... @@ -327,6 +327,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
327 327 icon: "add"
328 328 };
329 329
  330 + vm.addItemActionsOpen = false;
  331 +
  332 + vm.addItemActions = vm.config.addItemActions || [];
  333 +
330 334 vm.onGridInited = vm.config.onGridInited || function () {
331 335 };
332 336
... ...
... ... @@ -79,7 +79,7 @@
79 79 </tb-details-sidenav>
80 80 </section>
81 81
82   -<section layout="row" layout-wrap class="tb-footer-buttons md-fab ">
  82 +<section layout="row" layout-wrap class="tb-footer-buttons md-fab " layout-align="start end">
83 83 <md-button ng-disabled="loading" ng-show="vm.items.selectedCount > 0" class="tb-btn-footer md-accent md-hue-2 md-fab" ng-repeat="groupAction in vm.groupActionsList"
84 84 ng-click="groupAction.onAction($event, vm.items)" aria-label="{{ groupAction.name() }}">
85 85 <md-tooltip md-direction="top">
... ... @@ -93,10 +93,29 @@
93 93 </md-tooltip>
94 94 <ng-md-icon icon="arrow_drop_up"></ng-md-icon>
95 95 </md-button>
96   - <md-button ng-disabled="loading" ng-if="vm.addItemAction.name()" class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addItemAction.onAction($event)" aria-label="{{ vm.addItemAction.name() }}" >
  96 + <md-button ng-disabled="loading" ng-if="vm.addItemAction.name() && vm.addItemActions.length == 0" class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addItemAction.onAction($event)" aria-label="{{ vm.addItemAction.name() }}" >
97 97 <md-tooltip md-direction="top">
98 98 {{ vm.addItemAction.details() }}
99 99 </md-tooltip>
100 100 <ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon>
101 101 </md-button>
  102 + <md-fab-speed-dial ng-disabled="loading" ng-if="vm.addItemAction.name() && vm.addItemActions.length > 0" md-open="vm.addItemActionsOpen" class="md-scale" md-direction="up" ng-if="vm.addItemAction.name()">
  103 + <md-fab-trigger>
  104 + <md-button ng-disabled="loading" class="tb-btn-footer md-accent md-hue-2 md-fab" aria-label="{{ vm.addItemAction.name() }}" >
  105 + <md-tooltip md-direction="top">
  106 + {{ vm.addItemAction.details() }}
  107 + </md-tooltip>
  108 + <ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon>
  109 + </md-button>
  110 + </md-fab-trigger>
  111 + <md-fab-actions>
  112 + <md-button ng-disabled="loading" class="md-accent md-hue-2 md-fab" ng-repeat="addItemAction in vm.addItemActions"
  113 + ng-click="addItemAction.onAction($event)" aria-label="{{ addItemAction.name() }}" >
  114 + <md-tooltip md-direction="top">
  115 + {{ addItemAction.details() }}
  116 + </md-tooltip>
  117 + <ng-md-icon icon="{{addItemAction.icon}}"></ng-md-icon>
  118 + </md-button>
  119 + </md-fab-actions>
  120 + </md-fab-speed-dial>
102 121 </section>
\ No newline at end of file
... ...
... ... @@ -15,7 +15,7 @@
15 15 */
16 16 import React from 'react';
17 17 import ThingsboardBaseComponent from './json-form-base-component.jsx';
18   -import TextField from 'material-ui/TextField';
  18 +import NumberInput from 'material-ui-number-input';
19 19
20 20 class ThingsboardNumber extends React.Component {
21 21
... ... @@ -63,16 +63,18 @@ class ThingsboardNumber extends React.Component {
63 63 if (this.state.focused) {
64 64 fieldClass += " tb-focused";
65 65 }
  66 + var value = this.state.lastSuccessfulValue;
  67 + value = Number(value);
66 68
67 69 return (
68   - <TextField
  70 + <NumberInput
69 71 className={fieldClass}
70   - type={this.props.form.type}
  72 + strategy="allow"
71 73 floatingLabelText={this.props.form.title}
72 74 hintText={this.props.form.placeholder}
73 75 errorText={this.props.error}
74 76 onChange={this.preValidationCheck}
75   - defaultValue={this.state.lastSuccessfulValue}
  77 + defaultValue={value}
76 78 ref="numberField"
77 79 disabled={this.props.form.readonly}
78 80 onFocus={this.onFocus}
... ...
... ... @@ -102,10 +102,12 @@ export default function AddWidgetController($scope, widgetService, deviceService
102 102 controllerAs: 'vm',
103 103 templateUrl: deviceAliasesTemplate,
104 104 locals: {
105   - deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
106   - aliasToWidgetsMap: null,
107   - isSingleDevice: true,
108   - singleDeviceAlias: singleDeviceAlias
  105 + config: {
  106 + deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
  107 + widgets: null,
  108 + isSingleDevice: true,
  109 + singleDeviceAlias: singleDeviceAlias
  110 + }
109 111 },
110 112 parent: angular.element($document[0].body),
111 113 fullscreen: true,
... ...
... ... @@ -17,6 +17,7 @@
17 17 -->
18 18 <md-button ng-click="onAssignToCustomer({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button>
19 19 <md-button ng-click="onUnassignFromCustomer({event: $event})" ng-show="!isEdit && dashboardScope === 'customer'" class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button>
  20 +<md-button ng-click="onExportDashboard({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
20 21 <md-button ng-click="onDeleteDashboard({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
21 22
22 23 <md-content class="md-padding" layout="column">
... ...
... ... @@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
23 23
24 24 /*@ngInject*/
25 25 export default function DashboardController(types, widgetService, userService,
26   - dashboardService, itembuffer, hotkeys, $window, $rootScope,
  26 + dashboardService, itembuffer, importExport, hotkeys, $window, $rootScope,
27 27 $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
28 28
29 29 var user = userService.getCurrentUser();
... ... @@ -53,6 +53,8 @@ export default function DashboardController(types, widgetService, userService,
53 53 vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
54 54 vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
55 55 vm.editWidget = editWidget;
  56 + vm.exportWidget = exportWidget;
  57 + vm.importWidget = importWidget;
56 58 vm.isTenantAdmin = isTenantAdmin;
57 59 vm.loadDashboard = loadDashboard;
58 60 vm.noData = noData;
... ... @@ -210,44 +212,17 @@ export default function DashboardController(types, widgetService, userService,
210 212 }
211 213
212 214 function openDeviceAliases($event) {
213   - var aliasToWidgetsMap = {};
214   - var widgetsTitleList;
215   - for (var w in vm.widgets) {
216   - var widget = vm.widgets[w];
217   - if (widget.type === types.widgetType.rpc.value) {
218   - if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
219   - var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
220   - widgetsTitleList = aliasToWidgetsMap[targetDeviceAliasId];
221   - if (!widgetsTitleList) {
222   - widgetsTitleList = [];
223   - aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
224   - }
225   - widgetsTitleList.push(widget.config.title);
226   - }
227   - } else {
228   - for (var i in widget.config.datasources) {
229   - var datasource = widget.config.datasources[i];
230   - if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
231   - widgetsTitleList = aliasToWidgetsMap[datasource.deviceAliasId];
232   - if (!widgetsTitleList) {
233   - widgetsTitleList = [];
234   - aliasToWidgetsMap[datasource.deviceAliasId] = widgetsTitleList;
235   - }
236   - widgetsTitleList.push(widget.config.title);
237   - }
238   - }
239   - }
240   - }
241   -
242 215 $mdDialog.show({
243 216 controller: 'DeviceAliasesController',
244 217 controllerAs: 'vm',
245 218 templateUrl: deviceAliasesTemplate,
246 219 locals: {
247   - deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
248   - aliasToWidgetsMap: aliasToWidgetsMap,
249   - isSingleDevice: false,
250   - singleDeviceAlias: null
  220 + config: {
  221 + deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
  222 + widgets: vm.widgets,
  223 + isSingleDevice: false,
  224 + singleDeviceAlias: null
  225 + }
251 226 },
252 227 parent: angular.element($document[0].body),
253 228 skipHide: true,
... ... @@ -300,6 +275,16 @@ export default function DashboardController(types, widgetService, userService,
300 275 }
301 276 }
302 277
  278 + function exportWidget($event, widget) {
  279 + $event.stopPropagation();
  280 + importExport.exportWidget(vm.dashboard, widget);
  281 + }
  282 +
  283 + function importWidget($event) {
  284 + $event.stopPropagation();
  285 + importExport.importWidget($event, vm.dashboard);
  286 + }
  287 +
303 288 function widgetMouseDown($event, widget) {
304 289 if (vm.isEdit && !vm.isEditingWidget) {
305 290 vm.dashboardContainer.selectWidget(widget, 0);
... ... @@ -438,48 +423,7 @@ export default function DashboardController(types, widgetService, userService,
438 423 }
439 424
440 425 function copyWidget($event, widget) {
441   - var aliasesInfo = {
442   - datasourceAliases: {},
443   - targetDeviceAliases: {}
444   - };
445   - var originalColumns = 24;
446   - if (vm.dashboard.configuration.gridSettings &&
447   - vm.dashboard.configuration.gridSettings.columns) {
448   - originalColumns = vm.dashboard.configuration.gridSettings.columns;
449   - }
450   - if (widget.config && vm.dashboard.configuration
451   - && vm.dashboard.configuration.deviceAliases) {
452   - var deviceAlias;
453   - if (widget.config.datasources) {
454   - for (var i=0;i<widget.config.datasources.length;i++) {
455   - var datasource = widget.config.datasources[i];
456   - if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
457   - deviceAlias = vm.dashboard.configuration.deviceAliases[datasource.deviceAliasId];
458   - if (deviceAlias) {
459   - aliasesInfo.datasourceAliases[i] = {
460   - aliasName: deviceAlias.alias,
461   - deviceId: deviceAlias.deviceId
462   - }
463   - }
464   - }
465   - }
466   - }
467   - if (widget.config.targetDeviceAliasIds) {
468   - for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
469   - var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
470   - if (targetDeviceAliasId) {
471   - deviceAlias = vm.dashboard.configuration.deviceAliases[targetDeviceAliasId];
472   - if (deviceAlias) {
473   - aliasesInfo.targetDeviceAliases[i] = {
474   - aliasName: deviceAlias.alias,
475   - deviceId: deviceAlias.deviceId
476   - }
477   - }
478   - }
479   - }
480   - }
481   - }
482   - itembuffer.copyWidget(widget, aliasesInfo, originalColumns);
  426 + itembuffer.copyWidget(vm.dashboard, widget);
483 427 }
484 428
485 429 function helpLinkIdForWidgetType() {
... ...
... ... @@ -36,6 +36,7 @@ export default function DashboardDirective($compile, $templateCache) {
36 36 theForm: '=',
37 37 onAssignToCustomer: '&',
38 38 onUnassignFromCustomer: '&',
  39 + onExportDashboard: '&',
39 40 onDeleteDashboard: '&'
40 41 }
41 42 };
... ...
... ... @@ -80,8 +80,10 @@
80 80 is-mobile="vm.forceDashboardMobileMode"
81 81 is-mobile-disabled="vm.widgetEditMode"
82 82 is-edit-action-enabled="vm.isEdit || vm.widgetEditMode"
  83 + is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
83 84 is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
84 85 on-edit-widget="vm.editWidget(event, widget)"
  86 + on-export-widget="vm.exportWidget(event, widget)"
85 87 on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
86 88 on-widget-clicked="vm.widgetClicked(event, widget)"
87 89 on-widget-context-menu="vm.widgetContextMenu(event, widget)"
... ... @@ -180,15 +182,38 @@
180 182 </div>
181 183 </tb-details-sidenav>
182 184 <!-- </section> -->
183   - <section layout="row" layout-wrap class="tb-footer-buttons md-fab">
184   - <md-button ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
185   - class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
186   - aria-label="{{ 'dashboard.add-widget' | translate }}">
187   - <md-tooltip md-direction="top">
188   - {{ 'dashboard.add-widget' | translate }}
189   - </md-tooltip>
190   - <ng-md-icon icon="add"></ng-md-icon>
191   - </md-button>
  185 + <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
  186 + <md-fab-speed-dial ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
  187 + md-open="vm.addItemActionsOpen" class="md-scale" md-direction="up">
  188 + <md-fab-trigger>
  189 + <md-button ng-disabled="loading"
  190 + class="tb-btn-footer md-accent md-hue-2 md-fab"
  191 + aria-label="{{ 'dashboard.add-widget' | translate }}">
  192 + <md-tooltip md-direction="top">
  193 + {{ 'dashboard.add-widget' | translate }}
  194 + </md-tooltip>
  195 + <ng-md-icon icon="add"></ng-md-icon>
  196 + </md-button>
  197 + </md-fab-trigger>
  198 + <md-fab-actions>
  199 + <md-button ng-disabled="loading"
  200 + class="tmd-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
  201 + aria-label="{{ 'action.create' | translate }}">
  202 + <md-tooltip md-direction="top">
  203 + {{ 'dashboard.create-new-widget' | translate }}
  204 + </md-tooltip>
  205 + <ng-md-icon icon="insert_drive_file"></ng-md-icon>
  206 + </md-button>
  207 + <md-button ng-disabled="loading"
  208 + class="tmd-accent md-hue-2 md-fab" ng-click="vm.importWidget($event)"
  209 + aria-label="{{ 'action.import' | translate }}">
  210 + <md-tooltip md-direction="top">
  211 + {{ 'dashboard.import-widget' | translate }}
  212 + </md-tooltip>
  213 + <ng-md-icon icon="file_upload"></ng-md-icon>
  214 + </md-button>
  215 + </md-fab-actions>
  216 + </md-fab-speed-dial>
192 217 <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading && !vm.widgetEditMode" ng-disabled="loading"
193 218 class="tb-btn-footer md-accent md-hue-2 md-fab"
194 219 aria-label="{{ 'action.apply' | translate }}"
... ...
... ... @@ -23,7 +23,7 @@ import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.ht
23 23 /* eslint-enable import/no-unresolved, import/default */
24 24
25 25 /*@ngInject*/
26   -export default function DashboardsController(userService, dashboardService, customerService, $scope, $controller, $state, $stateParams, $mdDialog, $document, $q, $translate) {
  26 +export default function DashboardsController(userService, dashboardService, customerService, importExport, $scope, $controller, $state, $stateParams, $mdDialog, $document, $q, $translate) {
27 27
28 28 var customerId = $stateParams.customerId;
29 29
... ... @@ -86,6 +86,7 @@ export default function DashboardsController(userService, dashboardService, cust
86 86
87 87 vm.assignToCustomer = assignToCustomer;
88 88 vm.unassignFromCustomer = unassignFromCustomer;
  89 + vm.exportDashboard = exportDashboard;
89 90
90 91 initController();
91 92
... ... @@ -115,6 +116,14 @@ export default function DashboardsController(userService, dashboardService, cust
115 116 dashboardActionsList.push(
116 117 {
117 118 onAction: function ($event, item) {
  119 + exportDashboard($event, item);
  120 + },
  121 + name: function() { $translate.instant('action.export') },
  122 + details: function() { return $translate.instant('dashboard.export') },
  123 + icon: "file_download"
  124 + },
  125 + {
  126 + onAction: function ($event, item) {
118 127 assignToCustomer($event, [ item.id.id ]);
119 128 },
120 129 name: function() { return $translate.instant('action.assign') },
... ... @@ -158,7 +167,27 @@ export default function DashboardsController(userService, dashboardService, cust
158 167 }
159 168 );
160 169
161   -
  170 + vm.dashboardGridConfig.addItemActions = [];
  171 + vm.dashboardGridConfig.addItemActions.push({
  172 + onAction: function ($event) {
  173 + vm.grid.addItem($event);
  174 + },
  175 + name: function() { return $translate.instant('action.create') },
  176 + details: function() { return $translate.instant('dashboard.create-new-dashboard') },
  177 + icon: "insert_drive_file"
  178 + });
  179 + vm.dashboardGridConfig.addItemActions.push({
  180 + onAction: function ($event) {
  181 + importExport.importDashboard($event).then(
  182 + function() {
  183 + vm.grid.refreshList();
  184 + }
  185 + );
  186 + },
  187 + name: function() { return $translate.instant('action.import') },
  188 + details: function() { return $translate.instant('dashboard.import') },
  189 + icon: "file_upload"
  190 + });
162 191 } else if (vm.dashboardsScope === 'customer' || vm.dashboardsScope === 'customer_user') {
163 192 fetchDashboardsFunction = function (pageLink) {
164 193 return dashboardService.getCustomerDashboards(customerId, pageLink);
... ... @@ -344,6 +373,11 @@ export default function DashboardsController(userService, dashboardService, cust
344 373 });
345 374 }
346 375
  376 + function exportDashboard($event, dashboard) {
  377 + $event.stopPropagation();
  378 + importExport.exportDashboard(dashboard.id.id);
  379 + }
  380 +
347 381 function unassignDashboardsFromCustomer($event, items) {
348 382 var confirm = $mdDialog.confirm()
349 383 .targetEvent($event)
... ...
... ... @@ -25,5 +25,6 @@
25 25 the-form="vm.grid.detailsForm"
26 26 on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
27 27 on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)"
  28 + on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
28 29 on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
29 30 </tb-grid>
... ...
... ... @@ -17,16 +17,18 @@ import './device-aliases.scss';
17 17
18 18 /*@ngInject*/
19 19 export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
20   - deviceAliases, aliasToWidgetsMap, isSingleDevice, singleDeviceAlias) {
  20 + types, config) {
21 21
22 22 var vm = this;
23 23
24   - vm.isSingleDevice = isSingleDevice;
25   - vm.singleDeviceAlias = singleDeviceAlias;
  24 + vm.isSingleDevice = config.isSingleDevice;
  25 + vm.singleDeviceAlias = config.singleDeviceAlias;
26 26 vm.deviceAliases = [];
27   - vm.aliasToWidgetsMap = aliasToWidgetsMap;
28 27 vm.singleDevice = null;
29 28 vm.singleDeviceSearchText = '';
  29 + vm.title = config.customTitle ? config.customTitle : 'device.aliases';
  30 + vm.disableAdd = config.disableAdd;
  31 + vm.aliasToWidgetsMap = {};
30 32
31 33 vm.addAlias = addAlias;
32 34 vm.cancel = cancel;
... ... @@ -39,9 +41,48 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
39 41 initController();
40 42
41 43 function initController() {
42   - for (var aliasId in deviceAliases) {
43   - var alias = deviceAliases[aliasId].alias;
44   - var deviceId = deviceAliases[aliasId].deviceId;
  44 + var aliasId;
  45 + if (config.widgets) {
  46 + var widgetsTitleList, widget;
  47 + if (config.isSingleWidget && config.widgets.length == 1) {
  48 + widget = config.widgets[0];
  49 + widgetsTitleList = [widget.config.title];
  50 + for (aliasId in config.deviceAliases) {
  51 + vm.aliasToWidgetsMap[aliasId] = widgetsTitleList;
  52 + }
  53 + } else {
  54 + for (var w in config.widgets) {
  55 + widget = config.widgets[w];
  56 + if (widget.type === types.widgetType.rpc.value) {
  57 + if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
  58 + var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
  59 + widgetsTitleList = vm.aliasToWidgetsMap[targetDeviceAliasId];
  60 + if (!widgetsTitleList) {
  61 + widgetsTitleList = [];
  62 + vm.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
  63 + }
  64 + widgetsTitleList.push(widget.config.title);
  65 + }
  66 + } else {
  67 + for (var i in widget.config.datasources) {
  68 + var datasource = widget.config.datasources[i];
  69 + if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
  70 + widgetsTitleList = vm.aliasToWidgetsMap[datasource.deviceAliasId];
  71 + if (!widgetsTitleList) {
  72 + widgetsTitleList = [];
  73 + vm.aliasToWidgetsMap[datasource.deviceAliasId] = widgetsTitleList;
  74 + }
  75 + widgetsTitleList.push(widget.config.title);
  76 + }
  77 + }
  78 + }
  79 + }
  80 + }
  81 + }
  82 +
  83 + for (aliasId in config.deviceAliases) {
  84 + var alias = config.deviceAliases[aliasId].alias;
  85 + var deviceId = config.deviceAliases[aliasId].deviceId;
45 86 var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''};
46 87 if (deviceId) {
47 88 fetchAliasDevice(deviceAlias, deviceId);
... ...
... ... @@ -15,11 +15,11 @@
15 15 limitations under the License.
16 16
17 17 -->
18   -<md-dialog style="width: 700px;" aria-label="{{ 'device.aliases' | translate }}">
  18 +<md-dialog style="width: 700px;" aria-label="{{ vm.title | translate }}">
19 19 <form name="theForm" ng-submit="vm.save()">
20 20 <md-toolbar>
21 21 <div class="md-toolbar-tools">
22   - <h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : ('device.aliases' | translate) }}</h2>
  22 + <h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : (vm.title | translate) }}</h2>
23 23 <span flex></span>
24 24 <md-button class="md-icon-button" ng-click="vm.cancel()">
25 25 <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
... ... @@ -109,7 +109,7 @@
109 109 </div>
110 110 </div>
111 111 </div>
112   - <div ng-show="!vm.isSingleDevice" style="padding-bottom: 10px;">
  112 + <div ng-show="!vm.isSingleDevice && !vm.disableAdd" style="padding-bottom: 10px;">
113 113 <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
114 114 <md-tooltip md-direction="top">
115 115 {{ 'device.add-alias' | translate }}
... ...
... ... @@ -76,10 +76,12 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
76 76 controllerAs: 'vm',
77 77 templateUrl: deviceAliasesTemplate,
78 78 locals: {
79   - deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
80   - aliasToWidgetsMap: null,
81   - isSingleDevice: true,
82   - singleDeviceAlias: singleDeviceAlias
  79 + config: {
  80 + deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
  81 + widgets: null,
  82 + isSingleDevice: true,
  83 + singleDeviceAlias: singleDeviceAlias
  84 + }
83 85 },
84 86 parent: angular.element($document[0].body),
85 87 fullscreen: true,
... ...
... ... @@ -30,6 +30,7 @@ import thingsboardExpandFullscreen from '../components/expand-fullscreen.directi
30 30 import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
31 31 import thingsboardTypes from '../common/types.constant';
32 32 import thingsboardItemBuffer from '../services/item-buffer.service';
  33 +import thingsboardImportExport from '../import-export';
33 34
34 35 import DashboardRoutes from './dashboard.routes';
35 36 import DashboardsController from './dashboards.controller';
... ... @@ -47,6 +48,7 @@ export default angular.module('thingsboard.dashboard', [
47 48 gridster.name,
48 49 thingsboardTypes,
49 50 thingsboardItemBuffer,
  51 + thingsboardImportExport,
50 52 thingsboardGrid,
51 53 thingsboardApiWidget,
52 54 thingsboardApiUser,
... ...
... ... @@ -148,6 +148,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
148 148 $rootScope.loading = false;
149 149 }
150 150 var unhandled = false;
  151 + var ignoreErrors = rejection.config.ignoreErrors;
151 152 if (rejection.refreshTokenPending || rejection.status === 401) {
152 153 var errorCode = rejectionErrorCode(rejection);
153 154 if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
... ... @@ -156,13 +157,17 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
156 157 unhandled = true;
157 158 }
158 159 } else if (rejection.status === 403) {
159   - $rootScope.$broadcast('forbidden');
  160 + if (!ignoreErrors) {
  161 + $rootScope.$broadcast('forbidden');
  162 + }
160 163 } else if (rejection.status === 0 || rejection.status === -1) {
161 164 getToast().showError(getTranslate().instant('error.unable-to-connect'));
162 165 } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) {
163 166 if (rejection.status === 404) {
164   - getToast().showError(rejection.config.method + ": " + rejection.config.url + "<br/>" +
165   - rejection.status + ": " + rejection.statusText);
  167 + if (!ignoreErrors) {
  168 + getToast().showError(rejection.config.method + ": " + rejection.config.url + "<br/>" +
  169 + rejection.status + ": " + rejection.statusText);
  170 + }
166 171 } else {
167 172 unhandled = true;
168 173 }
... ...
  1 +/*
  2 + * Copyright © 2016 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 './import-dialog.scss';
  18 +
  19 +/*@ngInject*/
  20 +export default function ImportDialogController($scope, $mdDialog, toast, importTitle, importFileLabel) {
  21 +
  22 + var vm = this;
  23 +
  24 + vm.cancel = cancel;
  25 + vm.importFromJson = importFromJson;
  26 + vm.fileAdded = fileAdded;
  27 + vm.clearFile = clearFile;
  28 +
  29 + vm.importTitle = importTitle;
  30 + vm.importFileLabel = importFileLabel;
  31 +
  32 +
  33 + function cancel() {
  34 + $mdDialog.cancel();
  35 + }
  36 +
  37 + function fileAdded($file) {
  38 + if ($file.getExtension() === 'json') {
  39 + var reader = new FileReader();
  40 + reader.onload = function(event) {
  41 + $scope.$apply(function() {
  42 + if (event.target.result) {
  43 + $scope.theForm.$setDirty();
  44 + var importJson = event.target.result;
  45 + if (importJson && importJson.length > 0) {
  46 + try {
  47 + vm.importData = angular.fromJson(importJson);
  48 + vm.fileName = $file.name;
  49 + } catch (err) {
  50 + vm.fileName = null;
  51 + toast.showError(err.message);
  52 + }
  53 + }
  54 + }
  55 + });
  56 + };
  57 + reader.readAsText($file.file);
  58 + }
  59 + }
  60 +
  61 + function clearFile() {
  62 + $scope.theForm.$setDirty();
  63 + vm.fileName = null;
  64 + vm.importData = null;
  65 + }
  66 +
  67 + function importFromJson() {
  68 + $scope.theForm.$setPristine();
  69 + $mdDialog.hide(vm.importData);
  70 + }
  71 +}
... ...
  1 +/**
  2 + * Copyright © 2016 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 +$previewSize: 100px;
  17 +
  18 +.file-input {
  19 + display: none;
  20 +}
  21 +
  22 +.tb-container {
  23 + position: relative;
  24 + margin-top: 32px;
  25 + padding: 10px 0;
  26 +}
  27 +
  28 +.tb-file-select-container {
  29 + position: relative;
  30 + height: $previewSize;
  31 + width: 100%;
  32 +}
  33 +
  34 +.tb-file-preview {
  35 + max-width: $previewSize;
  36 + max-height: $previewSize;
  37 + width: auto;
  38 + height: auto;
  39 +}
  40 +
  41 +.tb-flow-drop {
  42 + position: relative;
  43 + border: dashed 2px;
  44 + height: $previewSize;
  45 + vertical-align: top;
  46 + padding: 0 8px;
  47 + overflow: hidden;
  48 + min-width: 300px;
  49 + label {
  50 + width: 100%;
  51 + font-size: 24px;
  52 + text-align: center;
  53 + position: absolute;
  54 + top: 50%;
  55 + left: 50%;
  56 + transform: translate(-50%,-50%);
  57 + }
  58 +}
  59 +
  60 +.tb-file-clear-container {
  61 + width: 48px;
  62 + height: $previewSize;
  63 + position: relative;
  64 + float: right;
  65 +}
  66 +.tb-file-clear-btn {
  67 + position: absolute !important;
  68 + top: 50%;
  69 + transform: translate(0%,-50%) !important;
  70 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<md-dialog aria-label="{{ vm.importTitle | translate }}">
  19 + <form name="theForm" ng-submit="vm.importFromJson()">
  20 + <md-toolbar>
  21 + <div class="md-toolbar-tools">
  22 + <h2 translate>{{ vm.importTitle }}</h2>
  23 + <span flex></span>
  24 + <md-button class="md-icon-button" ng-click="vm.cancel()">
  25 + <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
  26 + </md-button>
  27 + </div>
  28 + </md-toolbar>
  29 + <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
  30 + <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
  31 + <md-dialog-content>
  32 + <div class="md-dialog-content">
  33 + <fieldset ng-disabled="loading">
  34 + <div layout="column" layout-padding>
  35 + <div class="tb-container">
  36 + <label class="tb-label" translate>{{ vm.importFileLabel }}</label>
  37 + <div flow-init="{singleFile:true}"
  38 + flow-file-added="vm.fileAdded( $file )" class="tb-file-select-container">
  39 + <div class="tb-file-clear-container">
  40 + <md-button ng-click="vm.clearFile()"
  41 + class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
  42 + <md-tooltip md-direction="top">
  43 + {{ 'action.remove' | translate }}
  44 + </md-tooltip>
  45 + <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
  46 + close
  47 + </md-icon>
  48 + </md-button>
  49 + </div>
  50 + <div class="alert tb-flow-drop" flow-drop>
  51 + <label for="select" translate>import.drop-file</label>
  52 + <input class="file-input" flow-btn flow-attrs="{accept:'.json,application/json'}" id="select">
  53 + </div>
  54 + </div>
  55 + </div>
  56 + <div>
  57 + <div ng-show="!vm.fileName" translate>import.no-file</div>
  58 + <div ng-show="vm.fileName">{{ vm.fileName }}</div>
  59 + </div>
  60 + </div>
  61 + </fieldset>
  62 + </div>
  63 + </md-dialog-content>
  64 + <md-dialog-actions layout="row">
  65 + <span flex></span>
  66 + <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid || !vm.importData" type="submit" class="md-raised md-primary">
  67 + {{ 'action.import' | translate }}
  68 + </md-button>
  69 + <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
  70 + </md-dialog-actions>
  71 + </form>
  72 +</md-dialog>
... ...
  1 +/*
  2 + * Copyright © 2016 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 +/* eslint-disable import/no-unresolved, import/default */
  18 +
  19 +import importDialogTemplate from './import-dialog.tpl.html';
  20 +import deviceAliasesTemplate from '../dashboard/device-aliases.tpl.html';
  21 +
  22 +/* eslint-enable import/no-unresolved, import/default */
  23 +
  24 +
  25 +/* eslint-disable no-undef, angular/window-service, angular/document-service */
  26 +
  27 +/*@ngInject*/
  28 +export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, deviceService, dashboardService, toast) {
  29 +
  30 +
  31 + var service = {
  32 + exportDashboard: exportDashboard,
  33 + importDashboard: importDashboard,
  34 + exportWidget: exportWidget,
  35 + importWidget: importWidget
  36 + }
  37 +
  38 + return service;
  39 +
  40 + // Widget functions
  41 +
  42 + function exportWidget(dashboard, widget) {
  43 + var widgetItem = itembuffer.prepareWidgetItem(dashboard, widget);
  44 + var name = widgetItem.widget.config.title;
  45 + name = name.toLowerCase().replace(/\W/g,"_");
  46 + exportToPc(prepareExport(widgetItem), name + '.json');
  47 + }
  48 +
  49 + function importWidget($event, dashboard) {
  50 + openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then(
  51 + function success(widgetItem) {
  52 + if (!validateImportedWidget(widgetItem)) {
  53 + toast.showError($translate.instant('dashboard.invalid-widget-file-error'));
  54 + } else {
  55 + var widget = widgetItem.widget;
  56 + var aliasesInfo = widgetItem.aliasesInfo;
  57 + var originalColumns = widgetItem.originalColumns;
  58 +
  59 + var datasourceAliases = aliasesInfo.datasourceAliases;
  60 + var targetDeviceAliases = aliasesInfo.targetDeviceAliases;
  61 + if (datasourceAliases || targetDeviceAliases) {
  62 + var deviceAliases = {};
  63 + var datasourceAliasesMap = {};
  64 + var targetDeviceAliasesMap = {};
  65 + var aliasId = 1;
  66 + var datasourceIndex;
  67 + if (datasourceAliases) {
  68 + for (datasourceIndex in datasourceAliases) {
  69 + datasourceAliasesMap[aliasId] = datasourceIndex;
  70 + deviceAliases[aliasId] = {
  71 + alias: datasourceAliases[datasourceIndex].aliasName,
  72 + deviceId: datasourceAliases[datasourceIndex].deviceId
  73 + };
  74 + aliasId++;
  75 + }
  76 + }
  77 + if (targetDeviceAliases) {
  78 + for (datasourceIndex in targetDeviceAliases) {
  79 + targetDeviceAliasesMap[aliasId] = datasourceIndex;
  80 + deviceAliases[aliasId] = {
  81 + alias: targetDeviceAliases[datasourceIndex].aliasName,
  82 + deviceId: targetDeviceAliases[datasourceIndex].deviceId
  83 + };
  84 + aliasId++;
  85 + }
  86 + }
  87 +
  88 + var aliasIds = Object.keys(deviceAliases);
  89 + if (aliasIds.length > 0) {
  90 + processDeviceAliases(deviceAliases, aliasIds).then(
  91 + function(missingDeviceAliases) {
  92 + if (Object.keys(missingDeviceAliases).length > 0) {
  93 + editMissingAliases($event, [ widget ],
  94 + true, 'dashboard.widget-import-missing-aliases-title', missingDeviceAliases).then(
  95 + function success(updatedDeviceAliases) {
  96 + for (var aliasId in updatedDeviceAliases) {
  97 + var deviceAlias = updatedDeviceAliases[aliasId];
  98 + var datasourceIndex;
  99 + if (datasourceAliasesMap[aliasId]) {
  100 + datasourceIndex = datasourceAliasesMap[aliasId];
  101 + datasourceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
  102 + } else if (targetDeviceAliasesMap[aliasId]) {
  103 + datasourceIndex = targetDeviceAliasesMap[aliasId];
  104 + targetDeviceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
  105 + }
  106 + }
  107 + addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
  108 + },
  109 + function fail() {}
  110 + );
  111 + } else {
  112 + addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
  113 + }
  114 + }
  115 + );
  116 + } else {
  117 + addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
  118 + }
  119 + } else {
  120 + addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
  121 + }
  122 + }
  123 + },
  124 + function fail() {}
  125 + );
  126 + }
  127 +
  128 + function validateImportedWidget(widgetItem) {
  129 + if (angular.isUndefined(widgetItem.widget)
  130 + || angular.isUndefined(widgetItem.aliasesInfo)
  131 + || angular.isUndefined(widgetItem.originalColumns)) {
  132 + return false;
  133 + }
  134 + var widget = widgetItem.widget;
  135 + if (angular.isUndefined(widget.isSystemType) ||
  136 + angular.isUndefined(widget.bundleAlias) ||
  137 + angular.isUndefined(widget.typeAlias) ||
  138 + angular.isUndefined(widget.type)) {
  139 + return false;
  140 + }
  141 + return true;
  142 + }
  143 +
  144 + function addImportedWidget(dashboard, widget, aliasesInfo, originalColumns) {
  145 + itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, -1, -1);
  146 + }
  147 +
  148 + // Dashboard functions
  149 +
  150 + function exportDashboard(dashboardId) {
  151 + dashboardService.getDashboard(dashboardId).then(
  152 + function success(dashboard) {
  153 + var name = dashboard.title;
  154 + name = name.toLowerCase().replace(/\W/g,"_");
  155 + exportToPc(prepareExport(dashboard), name + '.json');
  156 + },
  157 + function fail(rejection) {
  158 + var message = rejection;
  159 + if (!message) {
  160 + message = $translate.instant('error.unknown-error');
  161 + }
  162 + toast.showError($translate.instant('dashboard.export-failed-error', {error: message}));
  163 + }
  164 + );
  165 + }
  166 +
  167 + function importDashboard($event) {
  168 + var deferred = $q.defer();
  169 + openImportDialog($event, 'dashboard.import', 'dashboard.dashboard-file').then(
  170 + function success(dashboard) {
  171 + if (!validateImportedDashboard(dashboard)) {
  172 + toast.showError($translate.instant('dashboard.invalid-dashboard-file-error'));
  173 + deferred.reject();
  174 + } else {
  175 + var deviceAliases = dashboard.configuration.deviceAliases;
  176 + if (deviceAliases) {
  177 + var aliasIds = Object.keys( deviceAliases );
  178 + if (aliasIds.length > 0) {
  179 + processDeviceAliases(deviceAliases, aliasIds).then(
  180 + function(missingDeviceAliases) {
  181 + if (Object.keys( missingDeviceAliases ).length > 0) {
  182 + editMissingAliases($event, dashboard.configuration.widgets,
  183 + false, 'dashboard.dashboard-import-missing-aliases-title', missingDeviceAliases).then(
  184 + function success(updatedDeviceAliases) {
  185 + for (var aliasId in updatedDeviceAliases) {
  186 + deviceAliases[aliasId] = updatedDeviceAliases[aliasId];
  187 + }
  188 + saveImportedDashboard(dashboard, deferred);
  189 + },
  190 + function fail() {
  191 + deferred.reject();
  192 + }
  193 + );
  194 + } else {
  195 + saveImportedDashboard(dashboard, deferred);
  196 + }
  197 + }
  198 + )
  199 + } else {
  200 + saveImportedDashboard(dashboard, deferred);
  201 + }
  202 + } else {
  203 + saveImportedDashboard(dashboard, deferred);
  204 + }
  205 + }
  206 + },
  207 + function fail() {
  208 + deferred.reject();
  209 + }
  210 + );
  211 + return deferred.promise;
  212 + }
  213 +
  214 + function saveImportedDashboard(dashboard, deferred) {
  215 + dashboardService.saveDashboard(dashboard).then(
  216 + function success() {
  217 + deferred.resolve();
  218 + },
  219 + function fail() {
  220 + deferred.reject();
  221 + }
  222 + )
  223 + }
  224 +
  225 + function validateImportedDashboard(dashboard) {
  226 + if (angular.isUndefined(dashboard.title) || angular.isUndefined(dashboard.configuration)) {
  227 + return false;
  228 + }
  229 + return true;
  230 + }
  231 +
  232 + function processDeviceAliases(deviceAliases, aliasIds) {
  233 + var deferred = $q.defer();
  234 + var missingDeviceAliases = {};
  235 + var index = -1;
  236 + checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
  237 + return deferred.promise;
  238 + }
  239 +
  240 + function checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
  241 + index++;
  242 + if (index == aliasIds.length) {
  243 + deferred.resolve(missingDeviceAliases);
  244 + } else {
  245 + checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
  246 + }
  247 + }
  248 +
  249 + function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
  250 + var aliasId = aliasIds[index];
  251 + var deviceAlias = deviceAliases[aliasId];
  252 + if (deviceAlias.deviceId) {
  253 + deviceService.getDevice(deviceAlias.deviceId, true).then(
  254 + function success() {
  255 + checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
  256 + },
  257 + function fail() {
  258 + var missingDeviceAlias = angular.copy(deviceAlias);
  259 + missingDeviceAlias.deviceId = null;
  260 + missingDeviceAliases[aliasId] = missingDeviceAlias;
  261 + checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
  262 + }
  263 + );
  264 + }
  265 + }
  266 +
  267 + function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) {
  268 + var deferred = $q.defer();
  269 + $mdDialog.show({
  270 + controller: 'DeviceAliasesController',
  271 + controllerAs: 'vm',
  272 + templateUrl: deviceAliasesTemplate,
  273 + locals: {
  274 + config: {
  275 + deviceAliases: missingDeviceAliases,
  276 + widgets: widgets,
  277 + isSingleWidget: isSingleWidget,
  278 + isSingleDevice: false,
  279 + singleDeviceAlias: null,
  280 + customTitle: customTitle,
  281 + disableAdd: true
  282 + }
  283 + },
  284 + parent: angular.element($document[0].body),
  285 + skipHide: true,
  286 + fullscreen: true,
  287 + targetEvent: $event
  288 + }).then(function (updatedDeviceAliases) {
  289 + deferred.resolve(updatedDeviceAliases);
  290 + }, function () {
  291 + deferred.reject();
  292 + });
  293 + return deferred.promise;
  294 + }
  295 +
  296 + // Common functions
  297 +
  298 + function prepareExport(data) {
  299 + var exportedData = angular.copy(data);
  300 + if (angular.isDefined(exportedData.id)) {
  301 + delete exportedData.id;
  302 + }
  303 + if (angular.isDefined(exportedData.createdTime)) {
  304 + delete exportedData.createdTime;
  305 + }
  306 + if (angular.isDefined(exportedData.tenantId)) {
  307 + delete exportedData.tenantId;
  308 + }
  309 + if (angular.isDefined(exportedData.customerId)) {
  310 + delete exportedData.customerId;
  311 + }
  312 + return exportedData;
  313 + }
  314 +
  315 + function exportToPc(data, filename) {
  316 + if (!data) {
  317 + $log.error('No data');
  318 + return;
  319 + }
  320 +
  321 + if (!filename) {
  322 + filename = 'download.json';
  323 + }
  324 +
  325 + if (angular.isObject(data)) {
  326 + data = angular.toJson(data, 2);
  327 + }
  328 +
  329 + var blob = new Blob([data], {type: 'text/json'});
  330 +
  331 + // FOR IE:
  332 +
  333 + if (window.navigator && window.navigator.msSaveOrOpenBlob) {
  334 + window.navigator.msSaveOrOpenBlob(blob, filename);
  335 + }
  336 + else{
  337 + var e = document.createEvent('MouseEvents'),
  338 + a = document.createElement('a');
  339 +
  340 + a.download = filename;
  341 + a.href = window.URL.createObjectURL(blob);
  342 + a.dataset.downloadurl = ['text/json', a.download, a.href].join(':');
  343 + e.initEvent('click', true, false, window,
  344 + 0, 0, 0, 0, 0, false, false, false, false, 0, null);
  345 + a.dispatchEvent(e);
  346 + }
  347 + }
  348 +
  349 + function openImportDialog($event, importTitle, importFileLabel) {
  350 + var deferred = $q.defer();
  351 + $mdDialog.show({
  352 + controller: 'ImportDialogController',
  353 + controllerAs: 'vm',
  354 + templateUrl: importDialogTemplate,
  355 + locals: {
  356 + importTitle: importTitle,
  357 + importFileLabel: importFileLabel
  358 + },
  359 + parent: angular.element($document[0].body),
  360 + skipHide: true,
  361 + fullscreen: true,
  362 + targetEvent: $event
  363 + }).then(function (importData) {
  364 + deferred.resolve(importData);
  365 + }, function () {
  366 + deferred.reject();
  367 + });
  368 + return deferred.promise;
  369 + }
  370 +
  371 +}
  372 +
  373 +/* eslint-enable no-undef, angular/window-service, angular/document-service */
... ...
  1 +/*
  2 + * Copyright © 2016 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 ImportExport from './import-export.service';
  18 +import ImportDialogController from './import-dialog.controller';
  19 +
  20 +
  21 +export default angular.module('thingsboard.importexport', [])
  22 + .factory('importExport', ImportExport)
  23 + .controller('ImportDialogController', ImportDialogController)
  24 + .name;
... ...
... ... @@ -25,11 +25,12 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
25 25 .name;
26 26
27 27 /*@ngInject*/
28   -function ItemBuffer(bufferStore) {
  28 +function ItemBuffer(bufferStore, types) {
29 29
30 30 const WIDGET_ITEM = "widget_item";
31 31
32 32 var service = {
  33 + prepareWidgetItem: prepareWidgetItem,
33 34 copyWidget: copyWidget,
34 35 hasWidget: hasWidget,
35 36 pasteWidget: pasteWidget,
... ... @@ -56,12 +57,57 @@ function ItemBuffer(bufferStore) {
56 57 }
57 58 **/
58 59
59   - function copyWidget(widget, aliasesInfo, originalColumns) {
60   - var widgetItem = {
  60 + function prepareWidgetItem(dashboard, widget) {
  61 + var aliasesInfo = {
  62 + datasourceAliases: {},
  63 + targetDeviceAliases: {}
  64 + };
  65 + var originalColumns = 24;
  66 + if (dashboard.configuration.gridSettings &&
  67 + dashboard.configuration.gridSettings.columns) {
  68 + originalColumns = dashboard.configuration.gridSettings.columns;
  69 + }
  70 + if (widget.config && dashboard.configuration
  71 + && dashboard.configuration.deviceAliases) {
  72 + var deviceAlias;
  73 + if (widget.config.datasources) {
  74 + for (var i=0;i<widget.config.datasources.length;i++) {
  75 + var datasource = widget.config.datasources[i];
  76 + if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
  77 + deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId];
  78 + if (deviceAlias) {
  79 + aliasesInfo.datasourceAliases[i] = {
  80 + aliasName: deviceAlias.alias,
  81 + deviceId: deviceAlias.deviceId
  82 + }
  83 + }
  84 + }
  85 + }
  86 + }
  87 + if (widget.config.targetDeviceAliasIds) {
  88 + for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
  89 + var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
  90 + if (targetDeviceAliasId) {
  91 + deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId];
  92 + if (deviceAlias) {
  93 + aliasesInfo.targetDeviceAliases[i] = {
  94 + aliasName: deviceAlias.alias,
  95 + deviceId: deviceAlias.deviceId
  96 + }
  97 + }
  98 + }
  99 + }
  100 + }
  101 + }
  102 + return {
61 103 widget: widget,
62 104 aliasesInfo: aliasesInfo,
63 105 originalColumns: originalColumns
64 106 }
  107 + }
  108 +
  109 + function copyWidget(dashboard, widget) {
  110 + var widgetItem = prepareWidgetItem(dashboard, widget);
65 111 bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
66 112 }
67 113
... ... @@ -69,7 +115,7 @@ function ItemBuffer(bufferStore) {
69 115 return bufferStore.get(WIDGET_ITEM);
70 116 }
71 117
72   - function pasteWidget(targetDasgboard, position) {
  118 + function pasteWidget(targetDashboard, position) {
73 119 var widgetItemJson = bufferStore.get(WIDGET_ITEM);
74 120 if (widgetItemJson) {
75 121 var widgetItem = angular.fromJson(widgetItemJson);
... ... @@ -82,7 +128,7 @@ function ItemBuffer(bufferStore) {
82 128 targetRow = position.row;
83 129 targetColumn = position.column;
84 130 }
85   - addWidgetToDashboard(targetDasgboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
  131 + addWidgetToDashboard(targetDashboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
86 132 }
87 133 }
88 134
... ...
... ... @@ -34,7 +34,13 @@ export default class TbAnalogueLinearGauge {
34 34 var majorTicksCount = settings.majorTicksCount || 10;
35 35 var total = maxValue-minValue;
36 36 var step = (total/majorTicksCount);
37   - step = parseFloat(parseFloat(step).toPrecision(12));
  37 +
  38 + var valueInt = settings.valueInt || 3;
  39 +
  40 + var valueDec = (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
  41 + ? settings.valueDec : 2;
  42 +
  43 + step = parseFloat(parseFloat(step).toFixed(valueDec));
38 44
39 45 var majorTicks = [];
40 46 var highlights = [];
... ... @@ -44,7 +50,7 @@ export default class TbAnalogueLinearGauge {
44 50 var majorTick = tick + minValue;
45 51 majorTicks.push(majorTick);
46 52 var nextTick = tick+step;
47   - nextTick = parseFloat(parseFloat(nextTick).toPrecision(12));
  53 + nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
48 54 if (tick<total) {
49 55 var highlightColor = tinycolor(keyColor);
50 56 var percent = tick/total;
... ... @@ -89,9 +95,8 @@ export default class TbAnalogueLinearGauge {
89 95 // borders
90 96
91 97 // number formats
92   - valueInt: settings.valueInt || 3,
93   - valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
94   - ? settings.valueDec : 2,
  98 + valueInt: valueInt,
  99 + valueDec: valueDec,
95 100 majorTicksInt: 1,
96 101 majorTicksDec: 0,
97 102
... ...
... ... @@ -35,7 +35,13 @@ export default class TbAnalogueRadialGauge {
35 35 var majorTicksCount = settings.majorTicksCount || 10;
36 36 var total = maxValue-minValue;
37 37 var step = (total/majorTicksCount);
38   - step = parseFloat(parseFloat(step).toPrecision(12));
  38 +
  39 + var valueInt = settings.valueInt || 3;
  40 +
  41 + var valueDec = (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
  42 + ? settings.valueDec : 2;
  43 +
  44 + step = parseFloat(parseFloat(step).toFixed(valueDec));
39 45
40 46 var majorTicks = [];
41 47 var highlights = [];
... ... @@ -44,7 +50,7 @@ export default class TbAnalogueRadialGauge {
44 50 while(tick<=maxValue) {
45 51 majorTicks.push(tick);
46 52 var nextTick = tick+step;
47   - nextTick = parseFloat(parseFloat(nextTick).toPrecision(12));
  53 + nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
48 54 if (tick<maxValue) {
49 55 var highlightColor = tinycolor(keyColor);
50 56 var percent = (tick-minValue)/total;
... ... @@ -86,9 +92,8 @@ export default class TbAnalogueRadialGauge {
86 92 //borderShadowWidth: (settings.showBorder !== false) ? 3 : 0,
87 93
88 94 // number formats
89   - valueInt: settings.valueInt || 3,
90   - valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
91   - ? settings.valueDec : 2,
  95 + valueInt: valueInt,
  96 + valueDec: valueDec,
92 97 majorTicksInt: 1,
93 98 majorTicksDec: 0,
94 99
... ...
... ... @@ -40,7 +40,9 @@
40 40 "refresh": "Refresh",
41 41 "undo": "Undo",
42 42 "copy": "Copy",
43   - "paste": "Paste"
  43 + "paste": "Paste",
  44 + "import": "Import",
  45 + "export": "Export"
44 46 },
45 47 "admin": {
46 48 "general": "General",
... ... @@ -214,7 +216,19 @@
214 216 "vertical-margin-required": "Vertical margin value is required.",
215 217 "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
216 218 "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
217   - "display-title": "Display dashboard title"
  219 + "display-title": "Display dashboard title",
  220 + "import": "Import dashboard",
  221 + "export": "Export dashboard",
  222 + "export-failed-error": "Unable to export dashboard: {error}",
  223 + "create-new-dashboard": "Create new dashboard",
  224 + "dashboard-file": "Dashboard file",
  225 + "invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.",
  226 + "dashboard-import-missing-aliases-title": "Select missing devices for dashboard aliases",
  227 + "create-new-widget": "Create new widget",
  228 + "import-widget": "Import widget",
  229 + "widget-file": "Widget file",
  230 + "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.",
  231 + "widget-import-missing-aliases-title": "Select missing devices used by widget"
218 232 },
219 233 "datakey": {
220 234 "settings": "Settings",
... ... @@ -370,6 +384,10 @@
370 384 "avatar": "Avatar",
371 385 "open-user-menu": "Open user menu"
372 386 },
  387 + "import": {
  388 + "no-file": "No file selected",
  389 + "drop-file": "Drop a JSON file or click to select a file to upload."
  390 + },
373 391 "item": {
374 392 "selected": "Selected"
375 393 },
... ... @@ -612,7 +630,8 @@
612 630 "widget-type-load-failed-error": "Failed to load widget type!",
613 631 "widget-template-load-failed-error": "Failed to load widget template!",
614 632 "add": "Add Widget",
615   - "undo": "Undo widget changes"
  633 + "undo": "Undo widget changes",
  634 + "export": "Export widget"
616 635 },
617 636 "widgets-bundle": {
618 637 "current": "Current bundle",
... ...