Commit 1305015585c5bc4581d22a7dfbbc7fdb4c912b0e
Committed by
GitHub
Merge pull request #33 from thingsboard/feature/import-export
Import/Export. Bug fixes.
Showing
28 changed files
with
928 additions
and
134 deletions
... | ... | @@ -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() { | ... | ... |
... | ... | @@ -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 | +} | ... | ... |
ui/src/app/import-export/import-dialog.scss
0 → 100644
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 */ | ... | ... |
ui/src/app/import-export/index.js
0 → 100644
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", | ... | ... |