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,6 +54,7 @@
54 "json-schema-defaults": "^0.2.0", 54 "json-schema-defaults": "^0.2.0",
55 "justgage": "^1.2.2", 55 "justgage": "^1.2.2",
56 "material-ui": "^0.16.1", 56 "material-ui": "^0.16.1",
  57 + "material-ui-number-input": "^5.0.16",
57 "md-color-picker": "^0.2.6", 58 "md-color-picker": "^0.2.6",
58 "mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5", 59 "mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5",
59 "moment": "^2.15.0", 60 "moment": "^2.15.0",
@@ -88,10 +88,10 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) { @@ -88,10 +88,10 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
88 return deferred.promise; 88 return deferred.promise;
89 } 89 }
90 90
91 - function getDevice(deviceId) { 91 + function getDevice(deviceId, ignoreErrors) {
92 var deferred = $q.defer(); 92 var deferred = $q.defer();
93 var url = '/api/device/' + deviceId; 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 deferred.resolve(response.data); 95 deferred.resolve(response.data);
96 }, function fail(response) { 96 }, function fail(response) {
97 deferred.reject(response.data); 97 deferred.reject(response.data);
@@ -58,8 +58,10 @@ function Dashboard() { @@ -58,8 +58,10 @@ function Dashboard() {
58 isMobile: '=', 58 isMobile: '=',
59 isMobileDisabled: '=?', 59 isMobileDisabled: '=?',
60 isEditActionEnabled: '=', 60 isEditActionEnabled: '=',
  61 + isExportActionEnabled: '=',
61 isRemoveActionEnabled: '=', 62 isRemoveActionEnabled: '=',
62 onEditWidget: '&?', 63 onEditWidget: '&?',
  64 + onExportWidget: '&?',
63 onRemoveWidget: '&?', 65 onRemoveWidget: '&?',
64 onWidgetMouseDown: '&?', 66 onWidgetMouseDown: '&?',
65 onWidgetClicked: '&?', 67 onWidgetClicked: '&?',
@@ -139,6 +141,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast @@ -139,6 +141,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
139 vm.showWidgetTitle = showWidgetTitle; 141 vm.showWidgetTitle = showWidgetTitle;
140 vm.hasTimewindow = hasTimewindow; 142 vm.hasTimewindow = hasTimewindow;
141 vm.editWidget = editWidget; 143 vm.editWidget = editWidget;
  144 + vm.exportWidget = exportWidget;
142 vm.removeWidget = removeWidget; 145 vm.removeWidget = removeWidget;
143 vm.loading = loading; 146 vm.loading = loading;
144 147
@@ -413,6 +416,16 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast @@ -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 function removeWidget($event, widget) { 429 function removeWidget($event, widget) {
417 resetWidgetClick(); 430 resetWidgetClick();
418 if ($event) { 431 if ($event) {
@@ -62,6 +62,18 @@ @@ -62,6 +62,18 @@
62 edit 62 edit
63 </md-icon> 63 </md-icon>
64 </md-button> 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 <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded" 77 <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
66 ng-disabled="vm.loading()" 78 ng-disabled="vm.loading()"
67 class="md-icon-button md-primary" 79 class="md-icon-button md-primary"
@@ -327,6 +327,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra @@ -327,6 +327,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
327 icon: "add" 327 icon: "add"
328 }; 328 };
329 329
  330 + vm.addItemActionsOpen = false;
  331 +
  332 + vm.addItemActions = vm.config.addItemActions || [];
  333 +
330 vm.onGridInited = vm.config.onGridInited || function () { 334 vm.onGridInited = vm.config.onGridInited || function () {
331 }; 335 };
332 336
@@ -79,7 +79,7 @@ @@ -79,7 +79,7 @@
79 </tb-details-sidenav> 79 </tb-details-sidenav>
80 </section> 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 <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" 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 ng-click="groupAction.onAction($event, vm.items)" aria-label="{{ groupAction.name() }}"> 84 ng-click="groupAction.onAction($event, vm.items)" aria-label="{{ groupAction.name() }}">
85 <md-tooltip md-direction="top"> 85 <md-tooltip md-direction="top">
@@ -93,10 +93,29 @@ @@ -93,10 +93,29 @@
93 </md-tooltip> 93 </md-tooltip>
94 <ng-md-icon icon="arrow_drop_up"></ng-md-icon> 94 <ng-md-icon icon="arrow_drop_up"></ng-md-icon>
95 </md-button> 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 <md-tooltip md-direction="top"> 97 <md-tooltip md-direction="top">
98 {{ vm.addItemAction.details() }} 98 {{ vm.addItemAction.details() }}
99 </md-tooltip> 99 </md-tooltip>
100 <ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon> 100 <ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon>
101 </md-button> 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 </section> 121 </section>
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 */ 15 */
16 import React from 'react'; 16 import React from 'react';
17 import ThingsboardBaseComponent from './json-form-base-component.jsx'; 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 class ThingsboardNumber extends React.Component { 20 class ThingsboardNumber extends React.Component {
21 21
@@ -63,16 +63,18 @@ class ThingsboardNumber extends React.Component { @@ -63,16 +63,18 @@ class ThingsboardNumber extends React.Component {
63 if (this.state.focused) { 63 if (this.state.focused) {
64 fieldClass += " tb-focused"; 64 fieldClass += " tb-focused";
65 } 65 }
  66 + var value = this.state.lastSuccessfulValue;
  67 + value = Number(value);
66 68
67 return ( 69 return (
68 - <TextField 70 + <NumberInput
69 className={fieldClass} 71 className={fieldClass}
70 - type={this.props.form.type} 72 + strategy="allow"
71 floatingLabelText={this.props.form.title} 73 floatingLabelText={this.props.form.title}
72 hintText={this.props.form.placeholder} 74 hintText={this.props.form.placeholder}
73 errorText={this.props.error} 75 errorText={this.props.error}
74 onChange={this.preValidationCheck} 76 onChange={this.preValidationCheck}
75 - defaultValue={this.state.lastSuccessfulValue} 77 + defaultValue={value}
76 ref="numberField" 78 ref="numberField"
77 disabled={this.props.form.readonly} 79 disabled={this.props.form.readonly}
78 onFocus={this.onFocus} 80 onFocus={this.onFocus}
@@ -102,10 +102,12 @@ export default function AddWidgetController($scope, widgetService, deviceService @@ -102,10 +102,12 @@ export default function AddWidgetController($scope, widgetService, deviceService
102 controllerAs: 'vm', 102 controllerAs: 'vm',
103 templateUrl: deviceAliasesTemplate, 103 templateUrl: deviceAliasesTemplate,
104 locals: { 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 parent: angular.element($document[0].body), 112 parent: angular.element($document[0].body),
111 fullscreen: true, 113 fullscreen: true,
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 --> 17 -->
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> 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 <md-button ng-click="onUnassignFromCustomer({event: $event})" ng-show="!isEdit && dashboardScope === 'customer'" class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button> 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 <md-button ng-click="onDeleteDashboard({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button> 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 <md-content class="md-padding" layout="column"> 23 <md-content class="md-padding" layout="column">
@@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html'; @@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
23 23
24 /*@ngInject*/ 24 /*@ngInject*/
25 export default function DashboardController(types, widgetService, userService, 25 export default function DashboardController(types, widgetService, userService,
26 - dashboardService, itembuffer, hotkeys, $window, $rootScope, 26 + dashboardService, itembuffer, importExport, hotkeys, $window, $rootScope,
27 $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) { 27 $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
28 28
29 var user = userService.getCurrentUser(); 29 var user = userService.getCurrentUser();
@@ -53,6 +53,8 @@ export default function DashboardController(types, widgetService, userService, @@ -53,6 +53,8 @@ export default function DashboardController(types, widgetService, userService,
53 vm.prepareDashboardContextMenu = prepareDashboardContextMenu; 53 vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
54 vm.prepareWidgetContextMenu = prepareWidgetContextMenu; 54 vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
55 vm.editWidget = editWidget; 55 vm.editWidget = editWidget;
  56 + vm.exportWidget = exportWidget;
  57 + vm.importWidget = importWidget;
56 vm.isTenantAdmin = isTenantAdmin; 58 vm.isTenantAdmin = isTenantAdmin;
57 vm.loadDashboard = loadDashboard; 59 vm.loadDashboard = loadDashboard;
58 vm.noData = noData; 60 vm.noData = noData;
@@ -210,44 +212,17 @@ export default function DashboardController(types, widgetService, userService, @@ -210,44 +212,17 @@ export default function DashboardController(types, widgetService, userService,
210 } 212 }
211 213
212 function openDeviceAliases($event) { 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 $mdDialog.show({ 215 $mdDialog.show({
243 controller: 'DeviceAliasesController', 216 controller: 'DeviceAliasesController',
244 controllerAs: 'vm', 217 controllerAs: 'vm',
245 templateUrl: deviceAliasesTemplate, 218 templateUrl: deviceAliasesTemplate,
246 locals: { 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 parent: angular.element($document[0].body), 227 parent: angular.element($document[0].body),
253 skipHide: true, 228 skipHide: true,
@@ -300,6 +275,16 @@ export default function DashboardController(types, widgetService, userService, @@ -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 function widgetMouseDown($event, widget) { 288 function widgetMouseDown($event, widget) {
304 if (vm.isEdit && !vm.isEditingWidget) { 289 if (vm.isEdit && !vm.isEditingWidget) {
305 vm.dashboardContainer.selectWidget(widget, 0); 290 vm.dashboardContainer.selectWidget(widget, 0);
@@ -438,48 +423,7 @@ export default function DashboardController(types, widgetService, userService, @@ -438,48 +423,7 @@ export default function DashboardController(types, widgetService, userService,
438 } 423 }
439 424
440 function copyWidget($event, widget) { 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 function helpLinkIdForWidgetType() { 429 function helpLinkIdForWidgetType() {
@@ -36,6 +36,7 @@ export default function DashboardDirective($compile, $templateCache) { @@ -36,6 +36,7 @@ export default function DashboardDirective($compile, $templateCache) {
36 theForm: '=', 36 theForm: '=',
37 onAssignToCustomer: '&', 37 onAssignToCustomer: '&',
38 onUnassignFromCustomer: '&', 38 onUnassignFromCustomer: '&',
  39 + onExportDashboard: '&',
39 onDeleteDashboard: '&' 40 onDeleteDashboard: '&'
40 } 41 }
41 }; 42 };
@@ -80,8 +80,10 @@ @@ -80,8 +80,10 @@
80 is-mobile="vm.forceDashboardMobileMode" 80 is-mobile="vm.forceDashboardMobileMode"
81 is-mobile-disabled="vm.widgetEditMode" 81 is-mobile-disabled="vm.widgetEditMode"
82 is-edit-action-enabled="vm.isEdit || vm.widgetEditMode" 82 is-edit-action-enabled="vm.isEdit || vm.widgetEditMode"
  83 + is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
83 is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode" 84 is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
84 on-edit-widget="vm.editWidget(event, widget)" 85 on-edit-widget="vm.editWidget(event, widget)"
  86 + on-export-widget="vm.exportWidget(event, widget)"
85 on-widget-mouse-down="vm.widgetMouseDown(event, widget)" 87 on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
86 on-widget-clicked="vm.widgetClicked(event, widget)" 88 on-widget-clicked="vm.widgetClicked(event, widget)"
87 on-widget-context-menu="vm.widgetContextMenu(event, widget)" 89 on-widget-context-menu="vm.widgetContextMenu(event, widget)"
@@ -180,15 +182,38 @@ @@ -180,15 +182,38 @@
180 </div> 182 </div>
181 </tb-details-sidenav> 183 </tb-details-sidenav>
182 <!-- </section> --> 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 <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading && !vm.widgetEditMode" ng-disabled="loading" 217 <md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading && !vm.widgetEditMode" ng-disabled="loading"
193 class="tb-btn-footer md-accent md-hue-2 md-fab" 218 class="tb-btn-footer md-accent md-hue-2 md-fab"
194 aria-label="{{ 'action.apply' | translate }}" 219 aria-label="{{ 'action.apply' | translate }}"
@@ -23,7 +23,7 @@ import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.ht @@ -23,7 +23,7 @@ import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.ht
23 /* eslint-enable import/no-unresolved, import/default */ 23 /* eslint-enable import/no-unresolved, import/default */
24 24
25 /*@ngInject*/ 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 var customerId = $stateParams.customerId; 28 var customerId = $stateParams.customerId;
29 29
@@ -86,6 +86,7 @@ export default function DashboardsController(userService, dashboardService, cust @@ -86,6 +86,7 @@ export default function DashboardsController(userService, dashboardService, cust
86 86
87 vm.assignToCustomer = assignToCustomer; 87 vm.assignToCustomer = assignToCustomer;
88 vm.unassignFromCustomer = unassignFromCustomer; 88 vm.unassignFromCustomer = unassignFromCustomer;
  89 + vm.exportDashboard = exportDashboard;
89 90
90 initController(); 91 initController();
91 92
@@ -115,6 +116,14 @@ export default function DashboardsController(userService, dashboardService, cust @@ -115,6 +116,14 @@ export default function DashboardsController(userService, dashboardService, cust
115 dashboardActionsList.push( 116 dashboardActionsList.push(
116 { 117 {
117 onAction: function ($event, item) { 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 assignToCustomer($event, [ item.id.id ]); 127 assignToCustomer($event, [ item.id.id ]);
119 }, 128 },
120 name: function() { return $translate.instant('action.assign') }, 129 name: function() { return $translate.instant('action.assign') },
@@ -158,7 +167,27 @@ export default function DashboardsController(userService, dashboardService, cust @@ -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 } else if (vm.dashboardsScope === 'customer' || vm.dashboardsScope === 'customer_user') { 191 } else if (vm.dashboardsScope === 'customer' || vm.dashboardsScope === 'customer_user') {
163 fetchDashboardsFunction = function (pageLink) { 192 fetchDashboardsFunction = function (pageLink) {
164 return dashboardService.getCustomerDashboards(customerId, pageLink); 193 return dashboardService.getCustomerDashboards(customerId, pageLink);
@@ -344,6 +373,11 @@ export default function DashboardsController(userService, dashboardService, cust @@ -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 function unassignDashboardsFromCustomer($event, items) { 381 function unassignDashboardsFromCustomer($event, items) {
348 var confirm = $mdDialog.confirm() 382 var confirm = $mdDialog.confirm()
349 .targetEvent($event) 383 .targetEvent($event)
@@ -25,5 +25,6 @@ @@ -25,5 +25,6 @@
25 the-form="vm.grid.detailsForm" 25 the-form="vm.grid.detailsForm"
26 on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])" 26 on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
27 on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)" 27 on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)"
  28 + on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
28 on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details> 29 on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
29 </tb-grid> 30 </tb-grid>
@@ -17,16 +17,18 @@ import './device-aliases.scss'; @@ -17,16 +17,18 @@ import './device-aliases.scss';
17 17
18 /*@ngInject*/ 18 /*@ngInject*/
19 export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate, 19 export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
20 - deviceAliases, aliasToWidgetsMap, isSingleDevice, singleDeviceAlias) { 20 + types, config) {
21 21
22 var vm = this; 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 vm.deviceAliases = []; 26 vm.deviceAliases = [];
27 - vm.aliasToWidgetsMap = aliasToWidgetsMap;  
28 vm.singleDevice = null; 27 vm.singleDevice = null;
29 vm.singleDeviceSearchText = ''; 28 vm.singleDeviceSearchText = '';
  29 + vm.title = config.customTitle ? config.customTitle : 'device.aliases';
  30 + vm.disableAdd = config.disableAdd;
  31 + vm.aliasToWidgetsMap = {};
30 32
31 vm.addAlias = addAlias; 33 vm.addAlias = addAlias;
32 vm.cancel = cancel; 34 vm.cancel = cancel;
@@ -39,9 +41,48 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m @@ -39,9 +41,48 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
39 initController(); 41 initController();
40 42
41 function initController() { 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 var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''}; 86 var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''};
46 if (deviceId) { 87 if (deviceId) {
47 fetchAliasDevice(deviceAlias, deviceId); 88 fetchAliasDevice(deviceAlias, deviceId);
@@ -15,11 +15,11 @@ @@ -15,11 +15,11 @@
15 limitations under the License. 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 <form name="theForm" ng-submit="vm.save()"> 19 <form name="theForm" ng-submit="vm.save()">
20 <md-toolbar> 20 <md-toolbar>
21 <div class="md-toolbar-tools"> 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 <span flex></span> 23 <span flex></span>
24 <md-button class="md-icon-button" ng-click="vm.cancel()"> 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> 25 <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
@@ -109,7 +109,7 @@ @@ -109,7 +109,7 @@
109 </div> 109 </div>
110 </div> 110 </div>
111 </div> 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 <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}"> 113 <md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
114 <md-tooltip md-direction="top"> 114 <md-tooltip md-direction="top">
115 {{ 'device.add-alias' | translate }} 115 {{ 'device.add-alias' | translate }}
@@ -76,10 +76,12 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ @@ -76,10 +76,12 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
76 controllerAs: 'vm', 76 controllerAs: 'vm',
77 templateUrl: deviceAliasesTemplate, 77 templateUrl: deviceAliasesTemplate,
78 locals: { 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 parent: angular.element($document[0].body), 86 parent: angular.element($document[0].body),
85 fullscreen: true, 87 fullscreen: true,
@@ -30,6 +30,7 @@ import thingsboardExpandFullscreen from '../components/expand-fullscreen.directi @@ -30,6 +30,7 @@ import thingsboardExpandFullscreen from '../components/expand-fullscreen.directi
30 import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive'; 30 import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
31 import thingsboardTypes from '../common/types.constant'; 31 import thingsboardTypes from '../common/types.constant';
32 import thingsboardItemBuffer from '../services/item-buffer.service'; 32 import thingsboardItemBuffer from '../services/item-buffer.service';
  33 +import thingsboardImportExport from '../import-export';
33 34
34 import DashboardRoutes from './dashboard.routes'; 35 import DashboardRoutes from './dashboard.routes';
35 import DashboardsController from './dashboards.controller'; 36 import DashboardsController from './dashboards.controller';
@@ -47,6 +48,7 @@ export default angular.module('thingsboard.dashboard', [ @@ -47,6 +48,7 @@ export default angular.module('thingsboard.dashboard', [
47 gridster.name, 48 gridster.name,
48 thingsboardTypes, 49 thingsboardTypes,
49 thingsboardItemBuffer, 50 thingsboardItemBuffer,
  51 + thingsboardImportExport,
50 thingsboardGrid, 52 thingsboardGrid,
51 thingsboardApiWidget, 53 thingsboardApiWidget,
52 thingsboardApiUser, 54 thingsboardApiUser,
@@ -148,6 +148,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { @@ -148,6 +148,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
148 $rootScope.loading = false; 148 $rootScope.loading = false;
149 } 149 }
150 var unhandled = false; 150 var unhandled = false;
  151 + var ignoreErrors = rejection.config.ignoreErrors;
151 if (rejection.refreshTokenPending || rejection.status === 401) { 152 if (rejection.refreshTokenPending || rejection.status === 401) {
152 var errorCode = rejectionErrorCode(rejection); 153 var errorCode = rejectionErrorCode(rejection);
153 if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) { 154 if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
@@ -156,13 +157,17 @@ export default function GlobalInterceptor($rootScope, $q, $injector) { @@ -156,13 +157,17 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
156 unhandled = true; 157 unhandled = true;
157 } 158 }
158 } else if (rejection.status === 403) { 159 } else if (rejection.status === 403) {
159 - $rootScope.$broadcast('forbidden'); 160 + if (!ignoreErrors) {
  161 + $rootScope.$broadcast('forbidden');
  162 + }
160 } else if (rejection.status === 0 || rejection.status === -1) { 163 } else if (rejection.status === 0 || rejection.status === -1) {
161 getToast().showError(getTranslate().instant('error.unable-to-connect')); 164 getToast().showError(getTranslate().instant('error.unable-to-connect'));
162 } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) { 165 } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) {
163 if (rejection.status === 404) { 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 } else { 171 } else {
167 unhandled = true; 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,11 +25,12 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
25 .name; 25 .name;
26 26
27 /*@ngInject*/ 27 /*@ngInject*/
28 -function ItemBuffer(bufferStore) { 28 +function ItemBuffer(bufferStore, types) {
29 29
30 const WIDGET_ITEM = "widget_item"; 30 const WIDGET_ITEM = "widget_item";
31 31
32 var service = { 32 var service = {
  33 + prepareWidgetItem: prepareWidgetItem,
33 copyWidget: copyWidget, 34 copyWidget: copyWidget,
34 hasWidget: hasWidget, 35 hasWidget: hasWidget,
35 pasteWidget: pasteWidget, 36 pasteWidget: pasteWidget,
@@ -56,12 +57,57 @@ function ItemBuffer(bufferStore) { @@ -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 widget: widget, 103 widget: widget,
62 aliasesInfo: aliasesInfo, 104 aliasesInfo: aliasesInfo,
63 originalColumns: originalColumns 105 originalColumns: originalColumns
64 } 106 }
  107 + }
  108 +
  109 + function copyWidget(dashboard, widget) {
  110 + var widgetItem = prepareWidgetItem(dashboard, widget);
65 bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem)); 111 bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
66 } 112 }
67 113
@@ -69,7 +115,7 @@ function ItemBuffer(bufferStore) { @@ -69,7 +115,7 @@ function ItemBuffer(bufferStore) {
69 return bufferStore.get(WIDGET_ITEM); 115 return bufferStore.get(WIDGET_ITEM);
70 } 116 }
71 117
72 - function pasteWidget(targetDasgboard, position) { 118 + function pasteWidget(targetDashboard, position) {
73 var widgetItemJson = bufferStore.get(WIDGET_ITEM); 119 var widgetItemJson = bufferStore.get(WIDGET_ITEM);
74 if (widgetItemJson) { 120 if (widgetItemJson) {
75 var widgetItem = angular.fromJson(widgetItemJson); 121 var widgetItem = angular.fromJson(widgetItemJson);
@@ -82,7 +128,7 @@ function ItemBuffer(bufferStore) { @@ -82,7 +128,7 @@ function ItemBuffer(bufferStore) {
82 targetRow = position.row; 128 targetRow = position.row;
83 targetColumn = position.column; 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,7 +34,13 @@ export default class TbAnalogueLinearGauge {
34 var majorTicksCount = settings.majorTicksCount || 10; 34 var majorTicksCount = settings.majorTicksCount || 10;
35 var total = maxValue-minValue; 35 var total = maxValue-minValue;
36 var step = (total/majorTicksCount); 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 var majorTicks = []; 45 var majorTicks = [];
40 var highlights = []; 46 var highlights = [];
@@ -44,7 +50,7 @@ export default class TbAnalogueLinearGauge { @@ -44,7 +50,7 @@ export default class TbAnalogueLinearGauge {
44 var majorTick = tick + minValue; 50 var majorTick = tick + minValue;
45 majorTicks.push(majorTick); 51 majorTicks.push(majorTick);
46 var nextTick = tick+step; 52 var nextTick = tick+step;
47 - nextTick = parseFloat(parseFloat(nextTick).toPrecision(12)); 53 + nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
48 if (tick<total) { 54 if (tick<total) {
49 var highlightColor = tinycolor(keyColor); 55 var highlightColor = tinycolor(keyColor);
50 var percent = tick/total; 56 var percent = tick/total;
@@ -89,9 +95,8 @@ export default class TbAnalogueLinearGauge { @@ -89,9 +95,8 @@ export default class TbAnalogueLinearGauge {
89 // borders 95 // borders
90 96
91 // number formats 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 majorTicksInt: 1, 100 majorTicksInt: 1,
96 majorTicksDec: 0, 101 majorTicksDec: 0,
97 102
@@ -35,7 +35,13 @@ export default class TbAnalogueRadialGauge { @@ -35,7 +35,13 @@ export default class TbAnalogueRadialGauge {
35 var majorTicksCount = settings.majorTicksCount || 10; 35 var majorTicksCount = settings.majorTicksCount || 10;
36 var total = maxValue-minValue; 36 var total = maxValue-minValue;
37 var step = (total/majorTicksCount); 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 var majorTicks = []; 46 var majorTicks = [];
41 var highlights = []; 47 var highlights = [];
@@ -44,7 +50,7 @@ export default class TbAnalogueRadialGauge { @@ -44,7 +50,7 @@ export default class TbAnalogueRadialGauge {
44 while(tick<=maxValue) { 50 while(tick<=maxValue) {
45 majorTicks.push(tick); 51 majorTicks.push(tick);
46 var nextTick = tick+step; 52 var nextTick = tick+step;
47 - nextTick = parseFloat(parseFloat(nextTick).toPrecision(12)); 53 + nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
48 if (tick<maxValue) { 54 if (tick<maxValue) {
49 var highlightColor = tinycolor(keyColor); 55 var highlightColor = tinycolor(keyColor);
50 var percent = (tick-minValue)/total; 56 var percent = (tick-minValue)/total;
@@ -86,9 +92,8 @@ export default class TbAnalogueRadialGauge { @@ -86,9 +92,8 @@ export default class TbAnalogueRadialGauge {
86 //borderShadowWidth: (settings.showBorder !== false) ? 3 : 0, 92 //borderShadowWidth: (settings.showBorder !== false) ? 3 : 0,
87 93
88 // number formats 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 majorTicksInt: 1, 97 majorTicksInt: 1,
93 majorTicksDec: 0, 98 majorTicksDec: 0,
94 99
@@ -40,7 +40,9 @@ @@ -40,7 +40,9 @@
40 "refresh": "Refresh", 40 "refresh": "Refresh",
41 "undo": "Undo", 41 "undo": "Undo",
42 "copy": "Copy", 42 "copy": "Copy",
43 - "paste": "Paste" 43 + "paste": "Paste",
  44 + "import": "Import",
  45 + "export": "Export"
44 }, 46 },
45 "admin": { 47 "admin": {
46 "general": "General", 48 "general": "General",
@@ -214,7 +216,19 @@ @@ -214,7 +216,19 @@
214 "vertical-margin-required": "Vertical margin value is required.", 216 "vertical-margin-required": "Vertical margin value is required.",
215 "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.", 217 "min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
216 "max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.", 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 "datakey": { 233 "datakey": {
220 "settings": "Settings", 234 "settings": "Settings",
@@ -370,6 +384,10 @@ @@ -370,6 +384,10 @@
370 "avatar": "Avatar", 384 "avatar": "Avatar",
371 "open-user-menu": "Open user menu" 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 "item": { 391 "item": {
374 "selected": "Selected" 392 "selected": "Selected"
375 }, 393 },
@@ -612,7 +630,8 @@ @@ -612,7 +630,8 @@
612 "widget-type-load-failed-error": "Failed to load widget type!", 630 "widget-type-load-failed-error": "Failed to load widget type!",
613 "widget-template-load-failed-error": "Failed to load widget template!", 631 "widget-template-load-failed-error": "Failed to load widget template!",
614 "add": "Add Widget", 632 "add": "Add Widget",
615 - "undo": "Undo widget changes" 633 + "undo": "Undo widget changes",
  634 + "export": "Export widget"
616 }, 635 },
617 "widgets-bundle": { 636 "widgets-bundle": {
618 "current": "Current bundle", 637 "current": "Current bundle",