thingsboard-aplcache
Changes
ui/package.json 1(+1 -0)
ui/src/app/api/device.service.js 4(+2 -2)
ui/src/app/components/dashboard.directive.js 13(+13 -0)
ui/src/app/components/dashboard.tpl.html 12(+12 -0)
ui/src/app/components/grid.tpl.html 23(+21 -2)
ui/src/app/dashboard/dashboard.controller.js 96(+20 -76)
ui/src/app/dashboard/dashboard.tpl.html 43(+34 -9)
ui/src/app/dashboard/index.js 2(+2 -0)
ui/src/app/global-interceptor.service.js 11(+8 -3)
ui/src/app/import-export/import-dialog.scss 70(+70 -0)
ui/src/app/import-export/import-export.service.js 373(+373 -0)
ui/src/app/import-export/index.js 24(+24 -0)
ui/src/app/services/item-buffer.service.js 56(+51 -5)
ui/src/locale/en_US.json 25(+22 -3)
Details
ui/package.json 1(+1 -0)
diff --git a/ui/package.json b/ui/package.json
index c1101d2..b39ae99 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -54,6 +54,7 @@
"json-schema-defaults": "^0.2.0",
"justgage": "^1.2.2",
"material-ui": "^0.16.1",
+ "material-ui-number-input": "^5.0.16",
"md-color-picker": "^0.2.6",
"mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5",
"moment": "^2.15.0",
ui/src/app/api/device.service.js 4(+2 -2)
diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js
index 27cf605..1a1d714 100644
--- a/ui/src/app/api/device.service.js
+++ b/ui/src/app/api/device.service.js
@@ -88,10 +88,10 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
return deferred.promise;
}
- function getDevice(deviceId) {
+ function getDevice(deviceId, ignoreErrors) {
var deferred = $q.defer();
var url = '/api/device/' + deviceId;
- $http.get(url, null).then(function success(response) {
+ $http.get(url, { ignoreErrors: ignoreErrors }).then(function success(response) {
deferred.resolve(response.data);
}, function fail(response) {
deferred.reject(response.data);
ui/src/app/components/dashboard.directive.js 13(+13 -0)
diff --git a/ui/src/app/components/dashboard.directive.js b/ui/src/app/components/dashboard.directive.js
index 7c5094a..c55995e 100644
--- a/ui/src/app/components/dashboard.directive.js
+++ b/ui/src/app/components/dashboard.directive.js
@@ -58,8 +58,10 @@ function Dashboard() {
isMobile: '=',
isMobileDisabled: '=?',
isEditActionEnabled: '=',
+ isExportActionEnabled: '=',
isRemoveActionEnabled: '=',
onEditWidget: '&?',
+ onExportWidget: '&?',
onRemoveWidget: '&?',
onWidgetMouseDown: '&?',
onWidgetClicked: '&?',
@@ -139,6 +141,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.showWidgetTitle = showWidgetTitle;
vm.hasTimewindow = hasTimewindow;
vm.editWidget = editWidget;
+ vm.exportWidget = exportWidget;
vm.removeWidget = removeWidget;
vm.loading = loading;
@@ -413,6 +416,16 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
}
}
+ function exportWidget ($event, widget) {
+ resetWidgetClick();
+ if ($event) {
+ $event.stopPropagation();
+ }
+ if (vm.isExportActionEnabled && vm.onExportWidget) {
+ vm.onExportWidget({event: $event, widget: widget});
+ }
+ }
+
function removeWidget($event, widget) {
resetWidgetClick();
if ($event) {
ui/src/app/components/dashboard.tpl.html 12(+12 -0)
diff --git a/ui/src/app/components/dashboard.tpl.html b/ui/src/app/components/dashboard.tpl.html
index c43caac..0ad43eb 100644
--- a/ui/src/app/components/dashboard.tpl.html
+++ b/ui/src/app/components/dashboard.tpl.html
@@ -62,6 +62,18 @@
edit
</md-icon>
</md-button>
+ <md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded"
+ ng-disabled="vm.loading()"
+ class="md-icon-button md-primary"
+ ng-click="vm.exportWidget($event, widget)"
+ aria-label="{{ 'widget.export' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'widget.export' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">
+ file_download
+ </md-icon>
+ </md-button>
<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button md-primary"
diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js
index ba8cdc1..67f29ed 100644
--- a/ui/src/app/components/grid.directive.js
+++ b/ui/src/app/components/grid.directive.js
@@ -327,6 +327,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
icon: "add"
};
+ vm.addItemActionsOpen = false;
+
+ vm.addItemActions = vm.config.addItemActions || [];
+
vm.onGridInited = vm.config.onGridInited || function () {
};
ui/src/app/components/grid.tpl.html 23(+21 -2)
diff --git a/ui/src/app/components/grid.tpl.html b/ui/src/app/components/grid.tpl.html
index 16d38f3..df3e54c 100644
--- a/ui/src/app/components/grid.tpl.html
+++ b/ui/src/app/components/grid.tpl.html
@@ -79,7 +79,7 @@
</tb-details-sidenav>
</section>
-<section layout="row" layout-wrap class="tb-footer-buttons md-fab ">
+<section layout="row" layout-wrap class="tb-footer-buttons md-fab " layout-align="start end">
<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"
ng-click="groupAction.onAction($event, vm.items)" aria-label="{{ groupAction.name() }}">
<md-tooltip md-direction="top">
@@ -93,10 +93,29 @@
</md-tooltip>
<ng-md-icon icon="arrow_drop_up"></ng-md-icon>
</md-button>
- <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() }}" >
+ <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() }}" >
<md-tooltip md-direction="top">
{{ vm.addItemAction.details() }}
</md-tooltip>
<ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon>
</md-button>
+ <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()">
+ <md-fab-trigger>
+ <md-button ng-disabled="loading" class="tb-btn-footer md-accent md-hue-2 md-fab" aria-label="{{ vm.addItemAction.name() }}" >
+ <md-tooltip md-direction="top">
+ {{ vm.addItemAction.details() }}
+ </md-tooltip>
+ <ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon>
+ </md-button>
+ </md-fab-trigger>
+ <md-fab-actions>
+ <md-button ng-disabled="loading" class="md-accent md-hue-2 md-fab" ng-repeat="addItemAction in vm.addItemActions"
+ ng-click="addItemAction.onAction($event)" aria-label="{{ addItemAction.name() }}" >
+ <md-tooltip md-direction="top">
+ {{ addItemAction.details() }}
+ </md-tooltip>
+ <ng-md-icon icon="{{addItemAction.icon}}"></ng-md-icon>
+ </md-button>
+ </md-fab-actions>
+ </md-fab-speed-dial>
</section>
\ No newline at end of file
diff --git a/ui/src/app/components/react/json-form-number.jsx b/ui/src/app/components/react/json-form-number.jsx
index 1922ca2..8d9296b 100644
--- a/ui/src/app/components/react/json-form-number.jsx
+++ b/ui/src/app/components/react/json-form-number.jsx
@@ -15,7 +15,7 @@
*/
import React from 'react';
import ThingsboardBaseComponent from './json-form-base-component.jsx';
-import TextField from 'material-ui/TextField';
+import NumberInput from 'material-ui-number-input';
class ThingsboardNumber extends React.Component {
@@ -63,16 +63,18 @@ class ThingsboardNumber extends React.Component {
if (this.state.focused) {
fieldClass += " tb-focused";
}
+ var value = this.state.lastSuccessfulValue;
+ value = Number(value);
return (
- <TextField
+ <NumberInput
className={fieldClass}
- type={this.props.form.type}
+ strategy="allow"
floatingLabelText={this.props.form.title}
hintText={this.props.form.placeholder}
errorText={this.props.error}
onChange={this.preValidationCheck}
- defaultValue={this.state.lastSuccessfulValue}
+ defaultValue={value}
ref="numberField"
disabled={this.props.form.readonly}
onFocus={this.onFocus}
diff --git a/ui/src/app/dashboard/add-widget.controller.js b/ui/src/app/dashboard/add-widget.controller.js
index ec77404..92a7235 100644
--- a/ui/src/app/dashboard/add-widget.controller.js
+++ b/ui/src/app/dashboard/add-widget.controller.js
@@ -102,10 +102,12 @@ export default function AddWidgetController($scope, widgetService, deviceService
controllerAs: 'vm',
templateUrl: deviceAliasesTemplate,
locals: {
- deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
- aliasToWidgetsMap: null,
- isSingleDevice: true,
- singleDeviceAlias: singleDeviceAlias
+ config: {
+ deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
+ widgets: null,
+ isSingleDevice: true,
+ singleDeviceAlias: singleDeviceAlias
+ }
},
parent: angular.element($document[0].body),
fullscreen: true,
ui/src/app/dashboard/dashboard.controller.js 96(+20 -76)
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 43225f5..8e85e74 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
/*@ngInject*/
export default function DashboardController(types, widgetService, userService,
- dashboardService, itembuffer, hotkeys, $window, $rootScope,
+ dashboardService, itembuffer, importExport, hotkeys, $window, $rootScope,
$scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
var user = userService.getCurrentUser();
@@ -53,6 +53,8 @@ export default function DashboardController(types, widgetService, userService,
vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
vm.editWidget = editWidget;
+ vm.exportWidget = exportWidget;
+ vm.importWidget = importWidget;
vm.isTenantAdmin = isTenantAdmin;
vm.loadDashboard = loadDashboard;
vm.noData = noData;
@@ -210,44 +212,17 @@ export default function DashboardController(types, widgetService, userService,
}
function openDeviceAliases($event) {
- var aliasToWidgetsMap = {};
- var widgetsTitleList;
- for (var w in vm.widgets) {
- var widget = vm.widgets[w];
- if (widget.type === types.widgetType.rpc.value) {
- if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
- var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
- widgetsTitleList = aliasToWidgetsMap[targetDeviceAliasId];
- if (!widgetsTitleList) {
- widgetsTitleList = [];
- aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
- }
- widgetsTitleList.push(widget.config.title);
- }
- } else {
- for (var i in widget.config.datasources) {
- var datasource = widget.config.datasources[i];
- if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
- widgetsTitleList = aliasToWidgetsMap[datasource.deviceAliasId];
- if (!widgetsTitleList) {
- widgetsTitleList = [];
- aliasToWidgetsMap[datasource.deviceAliasId] = widgetsTitleList;
- }
- widgetsTitleList.push(widget.config.title);
- }
- }
- }
- }
-
$mdDialog.show({
controller: 'DeviceAliasesController',
controllerAs: 'vm',
templateUrl: deviceAliasesTemplate,
locals: {
- deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
- aliasToWidgetsMap: aliasToWidgetsMap,
- isSingleDevice: false,
- singleDeviceAlias: null
+ config: {
+ deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
+ widgets: vm.widgets,
+ isSingleDevice: false,
+ singleDeviceAlias: null
+ }
},
parent: angular.element($document[0].body),
skipHide: true,
@@ -300,6 +275,16 @@ export default function DashboardController(types, widgetService, userService,
}
}
+ function exportWidget($event, widget) {
+ $event.stopPropagation();
+ importExport.exportWidget(vm.dashboard, widget);
+ }
+
+ function importWidget($event) {
+ $event.stopPropagation();
+ importExport.importWidget($event, vm.dashboard);
+ }
+
function widgetMouseDown($event, widget) {
if (vm.isEdit && !vm.isEditingWidget) {
vm.dashboardContainer.selectWidget(widget, 0);
@@ -438,48 +423,7 @@ export default function DashboardController(types, widgetService, userService,
}
function copyWidget($event, widget) {
- var aliasesInfo = {
- datasourceAliases: {},
- targetDeviceAliases: {}
- };
- var originalColumns = 24;
- if (vm.dashboard.configuration.gridSettings &&
- vm.dashboard.configuration.gridSettings.columns) {
- originalColumns = vm.dashboard.configuration.gridSettings.columns;
- }
- if (widget.config && vm.dashboard.configuration
- && vm.dashboard.configuration.deviceAliases) {
- var deviceAlias;
- if (widget.config.datasources) {
- for (var i=0;i<widget.config.datasources.length;i++) {
- var datasource = widget.config.datasources[i];
- if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
- deviceAlias = vm.dashboard.configuration.deviceAliases[datasource.deviceAliasId];
- if (deviceAlias) {
- aliasesInfo.datasourceAliases[i] = {
- aliasName: deviceAlias.alias,
- deviceId: deviceAlias.deviceId
- }
- }
- }
- }
- }
- if (widget.config.targetDeviceAliasIds) {
- for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
- var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
- if (targetDeviceAliasId) {
- deviceAlias = vm.dashboard.configuration.deviceAliases[targetDeviceAliasId];
- if (deviceAlias) {
- aliasesInfo.targetDeviceAliases[i] = {
- aliasName: deviceAlias.alias,
- deviceId: deviceAlias.deviceId
- }
- }
- }
- }
- }
- }
- itembuffer.copyWidget(widget, aliasesInfo, originalColumns);
+ itembuffer.copyWidget(vm.dashboard, widget);
}
function helpLinkIdForWidgetType() {
diff --git a/ui/src/app/dashboard/dashboard.directive.js b/ui/src/app/dashboard/dashboard.directive.js
index cd9243f..5d00cce 100644
--- a/ui/src/app/dashboard/dashboard.directive.js
+++ b/ui/src/app/dashboard/dashboard.directive.js
@@ -36,6 +36,7 @@ export default function DashboardDirective($compile, $templateCache) {
theForm: '=',
onAssignToCustomer: '&',
onUnassignFromCustomer: '&',
+ onExportDashboard: '&',
onDeleteDashboard: '&'
}
};
ui/src/app/dashboard/dashboard.tpl.html 43(+34 -9)
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 4d9c21e..16a6c16 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -80,8 +80,10 @@
is-mobile="vm.forceDashboardMobileMode"
is-mobile-disabled="vm.widgetEditMode"
is-edit-action-enabled="vm.isEdit || vm.widgetEditMode"
+ is-export-action-enabled="vm.isEdit && !vm.widgetEditMode"
is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
on-edit-widget="vm.editWidget(event, widget)"
+ on-export-widget="vm.exportWidget(event, widget)"
on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
on-widget-clicked="vm.widgetClicked(event, widget)"
on-widget-context-menu="vm.widgetContextMenu(event, widget)"
@@ -180,15 +182,38 @@
</div>
</tb-details-sidenav>
<!-- </section> -->
- <section layout="row" layout-wrap class="tb-footer-buttons md-fab">
- <md-button ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
- class="tb-btn-footer md-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
- aria-label="{{ 'dashboard.add-widget' | translate }}">
- <md-tooltip md-direction="top">
- {{ 'dashboard.add-widget' | translate }}
- </md-tooltip>
- <ng-md-icon icon="add"></ng-md-icon>
- </md-button>
+ <section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
+ <md-fab-speed-dial ng-disabled="loading" ng-show="!vm.isAddingWidget && vm.isEdit && !vm.widgetEditMode"
+ md-open="vm.addItemActionsOpen" class="md-scale" md-direction="up">
+ <md-fab-trigger>
+ <md-button ng-disabled="loading"
+ class="tb-btn-footer md-accent md-hue-2 md-fab"
+ aria-label="{{ 'dashboard.add-widget' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.add-widget' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="add"></ng-md-icon>
+ </md-button>
+ </md-fab-trigger>
+ <md-fab-actions>
+ <md-button ng-disabled="loading"
+ class="tmd-accent md-hue-2 md-fab" ng-click="vm.addWidget($event)"
+ aria-label="{{ 'action.create' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.create-new-widget' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="insert_drive_file"></ng-md-icon>
+ </md-button>
+ <md-button ng-disabled="loading"
+ class="tmd-accent md-hue-2 md-fab" ng-click="vm.importWidget($event)"
+ aria-label="{{ 'action.import' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'dashboard.import-widget' | translate }}
+ </md-tooltip>
+ <ng-md-icon icon="file_upload"></ng-md-icon>
+ </md-button>
+ </md-fab-actions>
+ </md-fab-speed-dial>
<md-button ng-if="vm.isTenantAdmin()" ng-show="vm.isEdit && !vm.isAddingWidget && !loading && !vm.widgetEditMode" ng-disabled="loading"
class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.apply' | translate }}"
diff --git a/ui/src/app/dashboard/dashboard-fieldset.tpl.html b/ui/src/app/dashboard/dashboard-fieldset.tpl.html
index 46e15b4..053a919 100644
--- a/ui/src/app/dashboard/dashboard-fieldset.tpl.html
+++ b/ui/src/app/dashboard/dashboard-fieldset.tpl.html
@@ -17,6 +17,7 @@
-->
<md-button ng-click="onAssignToCustomer({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.assign-to-customer' | translate }}</md-button>
<md-button ng-click="onUnassignFromCustomer({event: $event})" ng-show="!isEdit && dashboardScope === 'customer'" class="md-raised md-primary">{{ 'dashboard.unassign-from-customer' | translate }}</md-button>
+<md-button ng-click="onExportDashboard({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.export' | translate }}</md-button>
<md-button ng-click="onDeleteDashboard({event: $event})" ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.delete' | translate }}</md-button>
<md-content class="md-padding" layout="column">
diff --git a/ui/src/app/dashboard/dashboards.controller.js b/ui/src/app/dashboard/dashboards.controller.js
index 5a93ad3..8c84607 100644
--- a/ui/src/app/dashboard/dashboards.controller.js
+++ b/ui/src/app/dashboard/dashboards.controller.js
@@ -23,7 +23,7 @@ import addDashboardsToCustomerTemplate from './add-dashboards-to-customer.tpl.ht
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
-export default function DashboardsController(userService, dashboardService, customerService, $scope, $controller, $state, $stateParams, $mdDialog, $document, $q, $translate) {
+export default function DashboardsController(userService, dashboardService, customerService, importExport, $scope, $controller, $state, $stateParams, $mdDialog, $document, $q, $translate) {
var customerId = $stateParams.customerId;
@@ -86,6 +86,7 @@ export default function DashboardsController(userService, dashboardService, cust
vm.assignToCustomer = assignToCustomer;
vm.unassignFromCustomer = unassignFromCustomer;
+ vm.exportDashboard = exportDashboard;
initController();
@@ -115,6 +116,14 @@ export default function DashboardsController(userService, dashboardService, cust
dashboardActionsList.push(
{
onAction: function ($event, item) {
+ exportDashboard($event, item);
+ },
+ name: function() { $translate.instant('action.export') },
+ details: function() { return $translate.instant('dashboard.export') },
+ icon: "file_download"
+ },
+ {
+ onAction: function ($event, item) {
assignToCustomer($event, [ item.id.id ]);
},
name: function() { return $translate.instant('action.assign') },
@@ -158,7 +167,27 @@ export default function DashboardsController(userService, dashboardService, cust
}
);
-
+ vm.dashboardGridConfig.addItemActions = [];
+ vm.dashboardGridConfig.addItemActions.push({
+ onAction: function ($event) {
+ vm.grid.addItem($event);
+ },
+ name: function() { return $translate.instant('action.create') },
+ details: function() { return $translate.instant('dashboard.create-new-dashboard') },
+ icon: "insert_drive_file"
+ });
+ vm.dashboardGridConfig.addItemActions.push({
+ onAction: function ($event) {
+ importExport.importDashboard($event).then(
+ function() {
+ vm.grid.refreshList();
+ }
+ );
+ },
+ name: function() { return $translate.instant('action.import') },
+ details: function() { return $translate.instant('dashboard.import') },
+ icon: "file_upload"
+ });
} else if (vm.dashboardsScope === 'customer' || vm.dashboardsScope === 'customer_user') {
fetchDashboardsFunction = function (pageLink) {
return dashboardService.getCustomerDashboards(customerId, pageLink);
@@ -344,6 +373,11 @@ export default function DashboardsController(userService, dashboardService, cust
});
}
+ function exportDashboard($event, dashboard) {
+ $event.stopPropagation();
+ importExport.exportDashboard(dashboard.id.id);
+ }
+
function unassignDashboardsFromCustomer($event, items) {
var confirm = $mdDialog.confirm()
.targetEvent($event)
diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html
index ac8ccb2..c36c249 100644
--- a/ui/src/app/dashboard/dashboards.tpl.html
+++ b/ui/src/app/dashboard/dashboards.tpl.html
@@ -25,5 +25,6 @@
the-form="vm.grid.detailsForm"
on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])"
on-unassign-from-customer="vm.unassignFromCustomer(event, vm.grid.detailsConfig.currentItem)"
+ on-export-dashboard="vm.exportDashboard(event, vm.grid.detailsConfig.currentItem)"
on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
</tb-grid>
diff --git a/ui/src/app/dashboard/device-aliases.controller.js b/ui/src/app/dashboard/device-aliases.controller.js
index ab2b6f4..89fe6bf 100644
--- a/ui/src/app/dashboard/device-aliases.controller.js
+++ b/ui/src/app/dashboard/device-aliases.controller.js
@@ -17,16 +17,18 @@ import './device-aliases.scss';
/*@ngInject*/
export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
- deviceAliases, aliasToWidgetsMap, isSingleDevice, singleDeviceAlias) {
+ types, config) {
var vm = this;
- vm.isSingleDevice = isSingleDevice;
- vm.singleDeviceAlias = singleDeviceAlias;
+ vm.isSingleDevice = config.isSingleDevice;
+ vm.singleDeviceAlias = config.singleDeviceAlias;
vm.deviceAliases = [];
- vm.aliasToWidgetsMap = aliasToWidgetsMap;
vm.singleDevice = null;
vm.singleDeviceSearchText = '';
+ vm.title = config.customTitle ? config.customTitle : 'device.aliases';
+ vm.disableAdd = config.disableAdd;
+ vm.aliasToWidgetsMap = {};
vm.addAlias = addAlias;
vm.cancel = cancel;
@@ -39,9 +41,48 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
initController();
function initController() {
- for (var aliasId in deviceAliases) {
- var alias = deviceAliases[aliasId].alias;
- var deviceId = deviceAliases[aliasId].deviceId;
+ var aliasId;
+ if (config.widgets) {
+ var widgetsTitleList, widget;
+ if (config.isSingleWidget && config.widgets.length == 1) {
+ widget = config.widgets[0];
+ widgetsTitleList = [widget.config.title];
+ for (aliasId in config.deviceAliases) {
+ vm.aliasToWidgetsMap[aliasId] = widgetsTitleList;
+ }
+ } else {
+ for (var w in config.widgets) {
+ widget = config.widgets[w];
+ if (widget.type === types.widgetType.rpc.value) {
+ if (widget.config.targetDeviceAliasIds && widget.config.targetDeviceAliasIds.length > 0) {
+ var targetDeviceAliasId = widget.config.targetDeviceAliasIds[0];
+ widgetsTitleList = vm.aliasToWidgetsMap[targetDeviceAliasId];
+ if (!widgetsTitleList) {
+ widgetsTitleList = [];
+ vm.aliasToWidgetsMap[targetDeviceAliasId] = widgetsTitleList;
+ }
+ widgetsTitleList.push(widget.config.title);
+ }
+ } else {
+ for (var i in widget.config.datasources) {
+ var datasource = widget.config.datasources[i];
+ if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
+ widgetsTitleList = vm.aliasToWidgetsMap[datasource.deviceAliasId];
+ if (!widgetsTitleList) {
+ widgetsTitleList = [];
+ vm.aliasToWidgetsMap[datasource.deviceAliasId] = widgetsTitleList;
+ }
+ widgetsTitleList.push(widget.config.title);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ for (aliasId in config.deviceAliases) {
+ var alias = config.deviceAliases[aliasId].alias;
+ var deviceId = config.deviceAliases[aliasId].deviceId;
var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''};
if (deviceId) {
fetchAliasDevice(deviceAlias, deviceId);
diff --git a/ui/src/app/dashboard/device-aliases.tpl.html b/ui/src/app/dashboard/device-aliases.tpl.html
index bf5bb61..22421a3 100644
--- a/ui/src/app/dashboard/device-aliases.tpl.html
+++ b/ui/src/app/dashboard/device-aliases.tpl.html
@@ -15,11 +15,11 @@
limitations under the License.
-->
-<md-dialog style="width: 700px;" aria-label="{{ 'device.aliases' | translate }}">
+<md-dialog style="width: 700px;" aria-label="{{ vm.title | translate }}">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
- <h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : ('device.aliases' | translate) }}</h2>
+ <h2>{{ vm.isSingleDevice ? ('device.select-device-for-alias' | translate:vm.singleDeviceAlias ) : (vm.title | translate) }}</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
@@ -109,7 +109,7 @@
</div>
</div>
</div>
- <div ng-show="!vm.isSingleDevice" style="padding-bottom: 10px;">
+ <div ng-show="!vm.isSingleDevice && !vm.disableAdd" style="padding-bottom: 10px;">
<md-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
<md-tooltip md-direction="top">
{{ 'device.add-alias' | translate }}
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index 34b11f7..ecdf7ed 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -76,10 +76,12 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
controllerAs: 'vm',
templateUrl: deviceAliasesTemplate,
locals: {
- deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
- aliasToWidgetsMap: null,
- isSingleDevice: true,
- singleDeviceAlias: singleDeviceAlias
+ config: {
+ deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
+ widgets: null,
+ isSingleDevice: true,
+ singleDeviceAlias: singleDeviceAlias
+ }
},
parent: angular.element($document[0].body),
fullscreen: true,
ui/src/app/dashboard/index.js 2(+2 -0)
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js
index 461f56e..7dc9eff 100644
--- a/ui/src/app/dashboard/index.js
+++ b/ui/src/app/dashboard/index.js
@@ -30,6 +30,7 @@ import thingsboardExpandFullscreen from '../components/expand-fullscreen.directi
import thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
import thingsboardTypes from '../common/types.constant';
import thingsboardItemBuffer from '../services/item-buffer.service';
+import thingsboardImportExport from '../import-export';
import DashboardRoutes from './dashboard.routes';
import DashboardsController from './dashboards.controller';
@@ -47,6 +48,7 @@ export default angular.module('thingsboard.dashboard', [
gridster.name,
thingsboardTypes,
thingsboardItemBuffer,
+ thingsboardImportExport,
thingsboardGrid,
thingsboardApiWidget,
thingsboardApiUser,
ui/src/app/global-interceptor.service.js 11(+8 -3)
diff --git a/ui/src/app/global-interceptor.service.js b/ui/src/app/global-interceptor.service.js
index 7061152..06798ea 100644
--- a/ui/src/app/global-interceptor.service.js
+++ b/ui/src/app/global-interceptor.service.js
@@ -148,6 +148,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
$rootScope.loading = false;
}
var unhandled = false;
+ var ignoreErrors = rejection.config.ignoreErrors;
if (rejection.refreshTokenPending || rejection.status === 401) {
var errorCode = rejectionErrorCode(rejection);
if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
@@ -156,13 +157,17 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
unhandled = true;
}
} else if (rejection.status === 403) {
- $rootScope.$broadcast('forbidden');
+ if (!ignoreErrors) {
+ $rootScope.$broadcast('forbidden');
+ }
} else if (rejection.status === 0 || rejection.status === -1) {
getToast().showError(getTranslate().instant('error.unable-to-connect'));
} else if (!rejection.config.url.startsWith('/api/plugins/rpc')) {
if (rejection.status === 404) {
- getToast().showError(rejection.config.method + ": " + rejection.config.url + "<br/>" +
- rejection.status + ": " + rejection.statusText);
+ if (!ignoreErrors) {
+ getToast().showError(rejection.config.method + ": " + rejection.config.url + "<br/>" +
+ rejection.status + ": " + rejection.statusText);
+ }
} else {
unhandled = true;
}
diff --git a/ui/src/app/import-export/import-dialog.controller.js b/ui/src/app/import-export/import-dialog.controller.js
new file mode 100644
index 0000000..8a43fc7
--- /dev/null
+++ b/ui/src/app/import-export/import-dialog.controller.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './import-dialog.scss';
+
+/*@ngInject*/
+export default function ImportDialogController($scope, $mdDialog, toast, importTitle, importFileLabel) {
+
+ var vm = this;
+
+ vm.cancel = cancel;
+ vm.importFromJson = importFromJson;
+ vm.fileAdded = fileAdded;
+ vm.clearFile = clearFile;
+
+ vm.importTitle = importTitle;
+ vm.importFileLabel = importFileLabel;
+
+
+ function cancel() {
+ $mdDialog.cancel();
+ }
+
+ function fileAdded($file) {
+ if ($file.getExtension() === 'json') {
+ var reader = new FileReader();
+ reader.onload = function(event) {
+ $scope.$apply(function() {
+ if (event.target.result) {
+ $scope.theForm.$setDirty();
+ var importJson = event.target.result;
+ if (importJson && importJson.length > 0) {
+ try {
+ vm.importData = angular.fromJson(importJson);
+ vm.fileName = $file.name;
+ } catch (err) {
+ vm.fileName = null;
+ toast.showError(err.message);
+ }
+ }
+ }
+ });
+ };
+ reader.readAsText($file.file);
+ }
+ }
+
+ function clearFile() {
+ $scope.theForm.$setDirty();
+ vm.fileName = null;
+ vm.importData = null;
+ }
+
+ function importFromJson() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide(vm.importData);
+ }
+}
ui/src/app/import-export/import-dialog.scss 70(+70 -0)
diff --git a/ui/src/app/import-export/import-dialog.scss b/ui/src/app/import-export/import-dialog.scss
new file mode 100644
index 0000000..712126f
--- /dev/null
+++ b/ui/src/app/import-export/import-dialog.scss
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+$previewSize: 100px;
+
+.file-input {
+ display: none;
+}
+
+.tb-container {
+ position: relative;
+ margin-top: 32px;
+ padding: 10px 0;
+}
+
+.tb-file-select-container {
+ position: relative;
+ height: $previewSize;
+ width: 100%;
+}
+
+.tb-file-preview {
+ max-width: $previewSize;
+ max-height: $previewSize;
+ width: auto;
+ height: auto;
+}
+
+.tb-flow-drop {
+ position: relative;
+ border: dashed 2px;
+ height: $previewSize;
+ vertical-align: top;
+ padding: 0 8px;
+ overflow: hidden;
+ min-width: 300px;
+ label {
+ width: 100%;
+ font-size: 24px;
+ text-align: center;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%,-50%);
+ }
+}
+
+.tb-file-clear-container {
+ width: 48px;
+ height: $previewSize;
+ position: relative;
+ float: right;
+}
+.tb-file-clear-btn {
+ position: absolute !important;
+ top: 50%;
+ transform: translate(0%,-50%) !important;
+}
diff --git a/ui/src/app/import-export/import-dialog.tpl.html b/ui/src/app/import-export/import-dialog.tpl.html
new file mode 100644
index 0000000..2a8f316
--- /dev/null
+++ b/ui/src/app/import-export/import-dialog.tpl.html
@@ -0,0 +1,72 @@
+<!--
+
+ Copyright © 2016 The Thingsboard Authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<md-dialog aria-label="{{ vm.importTitle | translate }}">
+ <form name="theForm" ng-submit="vm.importFromJson()">
+ <md-toolbar>
+ <div class="md-toolbar-tools">
+ <h2 translate>{{ vm.importTitle }}</h2>
+ <span flex></span>
+ <md-button class="md-icon-button" ng-click="vm.cancel()">
+ <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-progress-linear class="md-warn" md-mode="indeterminate" ng-show="loading"></md-progress-linear>
+ <span style="min-height: 5px;" flex="" ng-show="!loading"></span>
+ <md-dialog-content>
+ <div class="md-dialog-content">
+ <fieldset ng-disabled="loading">
+ <div layout="column" layout-padding>
+ <div class="tb-container">
+ <label class="tb-label" translate>{{ vm.importFileLabel }}</label>
+ <div flow-init="{singleFile:true}"
+ flow-file-added="vm.fileAdded( $file )" class="tb-file-select-container">
+ <div class="tb-file-clear-container">
+ <md-button ng-click="vm.clearFile()"
+ class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
+ <md-tooltip md-direction="top">
+ {{ 'action.remove' | translate }}
+ </md-tooltip>
+ <md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">
+ close
+ </md-icon>
+ </md-button>
+ </div>
+ <div class="alert tb-flow-drop" flow-drop>
+ <label for="select" translate>import.drop-file</label>
+ <input class="file-input" flow-btn flow-attrs="{accept:'.json,application/json'}" id="select">
+ </div>
+ </div>
+ </div>
+ <div>
+ <div ng-show="!vm.fileName" translate>import.no-file</div>
+ <div ng-show="vm.fileName">{{ vm.fileName }}</div>
+ </div>
+ </div>
+ </fieldset>
+ </div>
+ </md-dialog-content>
+ <md-dialog-actions layout="row">
+ <span flex></span>
+ <md-button ng-disabled="loading || !theForm.$dirty || !theForm.$valid || !vm.importData" type="submit" class="md-raised md-primary">
+ {{ 'action.import' | translate }}
+ </md-button>
+ <md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}</md-button>
+ </md-dialog-actions>
+ </form>
+</md-dialog>
ui/src/app/import-export/import-export.service.js 373(+373 -0)
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
new file mode 100644
index 0000000..d409498
--- /dev/null
+++ b/ui/src/app/import-export/import-export.service.js
@@ -0,0 +1,373 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import importDialogTemplate from './import-dialog.tpl.html';
+import deviceAliasesTemplate from '../dashboard/device-aliases.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+
+/* eslint-disable no-undef, angular/window-service, angular/document-service */
+
+/*@ngInject*/
+export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, deviceService, dashboardService, toast) {
+
+
+ var service = {
+ exportDashboard: exportDashboard,
+ importDashboard: importDashboard,
+ exportWidget: exportWidget,
+ importWidget: importWidget
+ }
+
+ return service;
+
+ // Widget functions
+
+ function exportWidget(dashboard, widget) {
+ var widgetItem = itembuffer.prepareWidgetItem(dashboard, widget);
+ var name = widgetItem.widget.config.title;
+ name = name.toLowerCase().replace(/\W/g,"_");
+ exportToPc(prepareExport(widgetItem), name + '.json');
+ }
+
+ function importWidget($event, dashboard) {
+ openImportDialog($event, 'dashboard.import-widget', 'dashboard.widget-file').then(
+ function success(widgetItem) {
+ if (!validateImportedWidget(widgetItem)) {
+ toast.showError($translate.instant('dashboard.invalid-widget-file-error'));
+ } else {
+ var widget = widgetItem.widget;
+ var aliasesInfo = widgetItem.aliasesInfo;
+ var originalColumns = widgetItem.originalColumns;
+
+ var datasourceAliases = aliasesInfo.datasourceAliases;
+ var targetDeviceAliases = aliasesInfo.targetDeviceAliases;
+ if (datasourceAliases || targetDeviceAliases) {
+ var deviceAliases = {};
+ var datasourceAliasesMap = {};
+ var targetDeviceAliasesMap = {};
+ var aliasId = 1;
+ var datasourceIndex;
+ if (datasourceAliases) {
+ for (datasourceIndex in datasourceAliases) {
+ datasourceAliasesMap[aliasId] = datasourceIndex;
+ deviceAliases[aliasId] = {
+ alias: datasourceAliases[datasourceIndex].aliasName,
+ deviceId: datasourceAliases[datasourceIndex].deviceId
+ };
+ aliasId++;
+ }
+ }
+ if (targetDeviceAliases) {
+ for (datasourceIndex in targetDeviceAliases) {
+ targetDeviceAliasesMap[aliasId] = datasourceIndex;
+ deviceAliases[aliasId] = {
+ alias: targetDeviceAliases[datasourceIndex].aliasName,
+ deviceId: targetDeviceAliases[datasourceIndex].deviceId
+ };
+ aliasId++;
+ }
+ }
+
+ var aliasIds = Object.keys(deviceAliases);
+ if (aliasIds.length > 0) {
+ processDeviceAliases(deviceAliases, aliasIds).then(
+ function(missingDeviceAliases) {
+ if (Object.keys(missingDeviceAliases).length > 0) {
+ editMissingAliases($event, [ widget ],
+ true, 'dashboard.widget-import-missing-aliases-title', missingDeviceAliases).then(
+ function success(updatedDeviceAliases) {
+ for (var aliasId in updatedDeviceAliases) {
+ var deviceAlias = updatedDeviceAliases[aliasId];
+ var datasourceIndex;
+ if (datasourceAliasesMap[aliasId]) {
+ datasourceIndex = datasourceAliasesMap[aliasId];
+ datasourceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
+ } else if (targetDeviceAliasesMap[aliasId]) {
+ datasourceIndex = targetDeviceAliasesMap[aliasId];
+ targetDeviceAliases[datasourceIndex].deviceId = deviceAlias.deviceId;
+ }
+ }
+ addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+ },
+ function fail() {}
+ );
+ } else {
+ addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+ }
+ }
+ );
+ } else {
+ addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+ }
+ } else {
+ addImportedWidget(dashboard, widget, aliasesInfo, originalColumns);
+ }
+ }
+ },
+ function fail() {}
+ );
+ }
+
+ function validateImportedWidget(widgetItem) {
+ if (angular.isUndefined(widgetItem.widget)
+ || angular.isUndefined(widgetItem.aliasesInfo)
+ || angular.isUndefined(widgetItem.originalColumns)) {
+ return false;
+ }
+ var widget = widgetItem.widget;
+ if (angular.isUndefined(widget.isSystemType) ||
+ angular.isUndefined(widget.bundleAlias) ||
+ angular.isUndefined(widget.typeAlias) ||
+ angular.isUndefined(widget.type)) {
+ return false;
+ }
+ return true;
+ }
+
+ function addImportedWidget(dashboard, widget, aliasesInfo, originalColumns) {
+ itembuffer.addWidgetToDashboard(dashboard, widget, aliasesInfo, originalColumns, -1, -1);
+ }
+
+ // Dashboard functions
+
+ function exportDashboard(dashboardId) {
+ dashboardService.getDashboard(dashboardId).then(
+ function success(dashboard) {
+ var name = dashboard.title;
+ name = name.toLowerCase().replace(/\W/g,"_");
+ exportToPc(prepareExport(dashboard), name + '.json');
+ },
+ function fail(rejection) {
+ var message = rejection;
+ if (!message) {
+ message = $translate.instant('error.unknown-error');
+ }
+ toast.showError($translate.instant('dashboard.export-failed-error', {error: message}));
+ }
+ );
+ }
+
+ function importDashboard($event) {
+ var deferred = $q.defer();
+ openImportDialog($event, 'dashboard.import', 'dashboard.dashboard-file').then(
+ function success(dashboard) {
+ if (!validateImportedDashboard(dashboard)) {
+ toast.showError($translate.instant('dashboard.invalid-dashboard-file-error'));
+ deferred.reject();
+ } else {
+ var deviceAliases = dashboard.configuration.deviceAliases;
+ if (deviceAliases) {
+ var aliasIds = Object.keys( deviceAliases );
+ if (aliasIds.length > 0) {
+ processDeviceAliases(deviceAliases, aliasIds).then(
+ function(missingDeviceAliases) {
+ if (Object.keys( missingDeviceAliases ).length > 0) {
+ editMissingAliases($event, dashboard.configuration.widgets,
+ false, 'dashboard.dashboard-import-missing-aliases-title', missingDeviceAliases).then(
+ function success(updatedDeviceAliases) {
+ for (var aliasId in updatedDeviceAliases) {
+ deviceAliases[aliasId] = updatedDeviceAliases[aliasId];
+ }
+ saveImportedDashboard(dashboard, deferred);
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ } else {
+ saveImportedDashboard(dashboard, deferred);
+ }
+ }
+ )
+ } else {
+ saveImportedDashboard(dashboard, deferred);
+ }
+ } else {
+ saveImportedDashboard(dashboard, deferred);
+ }
+ }
+ },
+ function fail() {
+ deferred.reject();
+ }
+ );
+ return deferred.promise;
+ }
+
+ function saveImportedDashboard(dashboard, deferred) {
+ dashboardService.saveDashboard(dashboard).then(
+ function success() {
+ deferred.resolve();
+ },
+ function fail() {
+ deferred.reject();
+ }
+ )
+ }
+
+ function validateImportedDashboard(dashboard) {
+ if (angular.isUndefined(dashboard.title) || angular.isUndefined(dashboard.configuration)) {
+ return false;
+ }
+ return true;
+ }
+
+ function processDeviceAliases(deviceAliases, aliasIds) {
+ var deferred = $q.defer();
+ var missingDeviceAliases = {};
+ var index = -1;
+ checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+ return deferred.promise;
+ }
+
+ function checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
+ index++;
+ if (index == aliasIds.length) {
+ deferred.resolve(missingDeviceAliases);
+ } else {
+ checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+ }
+ }
+
+ function checkDeviceAlias(index, aliasIds, deviceAliases, missingDeviceAliases, deferred) {
+ var aliasId = aliasIds[index];
+ var deviceAlias = deviceAliases[aliasId];
+ if (deviceAlias.deviceId) {
+ deviceService.getDevice(deviceAlias.deviceId, true).then(
+ function success() {
+ checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+ },
+ function fail() {
+ var missingDeviceAlias = angular.copy(deviceAlias);
+ missingDeviceAlias.deviceId = null;
+ missingDeviceAliases[aliasId] = missingDeviceAlias;
+ checkNextDeviceAliasOrComplete(index, aliasIds, deviceAliases, missingDeviceAliases, deferred);
+ }
+ );
+ }
+ }
+
+ function editMissingAliases($event, widgets, isSingleWidget, customTitle, missingDeviceAliases) {
+ var deferred = $q.defer();
+ $mdDialog.show({
+ controller: 'DeviceAliasesController',
+ controllerAs: 'vm',
+ templateUrl: deviceAliasesTemplate,
+ locals: {
+ config: {
+ deviceAliases: missingDeviceAliases,
+ widgets: widgets,
+ isSingleWidget: isSingleWidget,
+ isSingleDevice: false,
+ singleDeviceAlias: null,
+ customTitle: customTitle,
+ disableAdd: true
+ }
+ },
+ parent: angular.element($document[0].body),
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (updatedDeviceAliases) {
+ deferred.resolve(updatedDeviceAliases);
+ }, function () {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+ // Common functions
+
+ function prepareExport(data) {
+ var exportedData = angular.copy(data);
+ if (angular.isDefined(exportedData.id)) {
+ delete exportedData.id;
+ }
+ if (angular.isDefined(exportedData.createdTime)) {
+ delete exportedData.createdTime;
+ }
+ if (angular.isDefined(exportedData.tenantId)) {
+ delete exportedData.tenantId;
+ }
+ if (angular.isDefined(exportedData.customerId)) {
+ delete exportedData.customerId;
+ }
+ return exportedData;
+ }
+
+ function exportToPc(data, filename) {
+ if (!data) {
+ $log.error('No data');
+ return;
+ }
+
+ if (!filename) {
+ filename = 'download.json';
+ }
+
+ if (angular.isObject(data)) {
+ data = angular.toJson(data, 2);
+ }
+
+ var blob = new Blob([data], {type: 'text/json'});
+
+ // FOR IE:
+
+ if (window.navigator && window.navigator.msSaveOrOpenBlob) {
+ window.navigator.msSaveOrOpenBlob(blob, filename);
+ }
+ else{
+ var e = document.createEvent('MouseEvents'),
+ a = document.createElement('a');
+
+ a.download = filename;
+ a.href = window.URL.createObjectURL(blob);
+ a.dataset.downloadurl = ['text/json', a.download, a.href].join(':');
+ e.initEvent('click', true, false, window,
+ 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ a.dispatchEvent(e);
+ }
+ }
+
+ function openImportDialog($event, importTitle, importFileLabel) {
+ var deferred = $q.defer();
+ $mdDialog.show({
+ controller: 'ImportDialogController',
+ controllerAs: 'vm',
+ templateUrl: importDialogTemplate,
+ locals: {
+ importTitle: importTitle,
+ importFileLabel: importFileLabel
+ },
+ parent: angular.element($document[0].body),
+ skipHide: true,
+ fullscreen: true,
+ targetEvent: $event
+ }).then(function (importData) {
+ deferred.resolve(importData);
+ }, function () {
+ deferred.reject();
+ });
+ return deferred.promise;
+ }
+
+}
+
+/* eslint-enable no-undef, angular/window-service, angular/document-service */
ui/src/app/import-export/index.js 24(+24 -0)
diff --git a/ui/src/app/import-export/index.js b/ui/src/app/import-export/index.js
new file mode 100644
index 0000000..6d76229
--- /dev/null
+++ b/ui/src/app/import-export/index.js
@@ -0,0 +1,24 @@
+/*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import ImportExport from './import-export.service';
+import ImportDialogController from './import-dialog.controller';
+
+
+export default angular.module('thingsboard.importexport', [])
+ .factory('importExport', ImportExport)
+ .controller('ImportDialogController', ImportDialogController)
+ .name;
ui/src/app/services/item-buffer.service.js 56(+51 -5)
diff --git a/ui/src/app/services/item-buffer.service.js b/ui/src/app/services/item-buffer.service.js
index 56d8d6f..2e59bb6 100644
--- a/ui/src/app/services/item-buffer.service.js
+++ b/ui/src/app/services/item-buffer.service.js
@@ -25,11 +25,12 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
.name;
/*@ngInject*/
-function ItemBuffer(bufferStore) {
+function ItemBuffer(bufferStore, types) {
const WIDGET_ITEM = "widget_item";
var service = {
+ prepareWidgetItem: prepareWidgetItem,
copyWidget: copyWidget,
hasWidget: hasWidget,
pasteWidget: pasteWidget,
@@ -56,12 +57,57 @@ function ItemBuffer(bufferStore) {
}
**/
- function copyWidget(widget, aliasesInfo, originalColumns) {
- var widgetItem = {
+ function prepareWidgetItem(dashboard, widget) {
+ var aliasesInfo = {
+ datasourceAliases: {},
+ targetDeviceAliases: {}
+ };
+ var originalColumns = 24;
+ if (dashboard.configuration.gridSettings &&
+ dashboard.configuration.gridSettings.columns) {
+ originalColumns = dashboard.configuration.gridSettings.columns;
+ }
+ if (widget.config && dashboard.configuration
+ && dashboard.configuration.deviceAliases) {
+ var deviceAlias;
+ if (widget.config.datasources) {
+ for (var i=0;i<widget.config.datasources.length;i++) {
+ var datasource = widget.config.datasources[i];
+ if (datasource.type === types.datasourceType.device && datasource.deviceAliasId) {
+ deviceAlias = dashboard.configuration.deviceAliases[datasource.deviceAliasId];
+ if (deviceAlias) {
+ aliasesInfo.datasourceAliases[i] = {
+ aliasName: deviceAlias.alias,
+ deviceId: deviceAlias.deviceId
+ }
+ }
+ }
+ }
+ }
+ if (widget.config.targetDeviceAliasIds) {
+ for (i=0;i<widget.config.targetDeviceAliasIds.length;i++) {
+ var targetDeviceAliasId = widget.config.targetDeviceAliasIds[i];
+ if (targetDeviceAliasId) {
+ deviceAlias = dashboard.configuration.deviceAliases[targetDeviceAliasId];
+ if (deviceAlias) {
+ aliasesInfo.targetDeviceAliases[i] = {
+ aliasName: deviceAlias.alias,
+ deviceId: deviceAlias.deviceId
+ }
+ }
+ }
+ }
+ }
+ }
+ return {
widget: widget,
aliasesInfo: aliasesInfo,
originalColumns: originalColumns
}
+ }
+
+ function copyWidget(dashboard, widget) {
+ var widgetItem = prepareWidgetItem(dashboard, widget);
bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
}
@@ -69,7 +115,7 @@ function ItemBuffer(bufferStore) {
return bufferStore.get(WIDGET_ITEM);
}
- function pasteWidget(targetDasgboard, position) {
+ function pasteWidget(targetDashboard, position) {
var widgetItemJson = bufferStore.get(WIDGET_ITEM);
if (widgetItemJson) {
var widgetItem = angular.fromJson(widgetItemJson);
@@ -82,7 +128,7 @@ function ItemBuffer(bufferStore) {
targetRow = position.row;
targetColumn = position.column;
}
- addWidgetToDashboard(targetDasgboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
+ addWidgetToDashboard(targetDashboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
}
}
diff --git a/ui/src/app/widget/lib/analogue-linear-gauge.js b/ui/src/app/widget/lib/analogue-linear-gauge.js
index 72b5d6e..54ddeb9 100644
--- a/ui/src/app/widget/lib/analogue-linear-gauge.js
+++ b/ui/src/app/widget/lib/analogue-linear-gauge.js
@@ -34,7 +34,13 @@ export default class TbAnalogueLinearGauge {
var majorTicksCount = settings.majorTicksCount || 10;
var total = maxValue-minValue;
var step = (total/majorTicksCount);
- step = parseFloat(parseFloat(step).toPrecision(12));
+
+ var valueInt = settings.valueInt || 3;
+
+ var valueDec = (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
+ ? settings.valueDec : 2;
+
+ step = parseFloat(parseFloat(step).toFixed(valueDec));
var majorTicks = [];
var highlights = [];
@@ -44,7 +50,7 @@ export default class TbAnalogueLinearGauge {
var majorTick = tick + minValue;
majorTicks.push(majorTick);
var nextTick = tick+step;
- nextTick = parseFloat(parseFloat(nextTick).toPrecision(12));
+ nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
if (tick<total) {
var highlightColor = tinycolor(keyColor);
var percent = tick/total;
@@ -89,9 +95,8 @@ export default class TbAnalogueLinearGauge {
// borders
// number formats
- valueInt: settings.valueInt || 3,
- valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
- ? settings.valueDec : 2,
+ valueInt: valueInt,
+ valueDec: valueDec,
majorTicksInt: 1,
majorTicksDec: 0,
diff --git a/ui/src/app/widget/lib/analogue-radial-gauge.js b/ui/src/app/widget/lib/analogue-radial-gauge.js
index c970b9d..1ed8aa4 100644
--- a/ui/src/app/widget/lib/analogue-radial-gauge.js
+++ b/ui/src/app/widget/lib/analogue-radial-gauge.js
@@ -35,7 +35,13 @@ export default class TbAnalogueRadialGauge {
var majorTicksCount = settings.majorTicksCount || 10;
var total = maxValue-minValue;
var step = (total/majorTicksCount);
- step = parseFloat(parseFloat(step).toPrecision(12));
+
+ var valueInt = settings.valueInt || 3;
+
+ var valueDec = (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
+ ? settings.valueDec : 2;
+
+ step = parseFloat(parseFloat(step).toFixed(valueDec));
var majorTicks = [];
var highlights = [];
@@ -44,7 +50,7 @@ export default class TbAnalogueRadialGauge {
while(tick<=maxValue) {
majorTicks.push(tick);
var nextTick = tick+step;
- nextTick = parseFloat(parseFloat(nextTick).toPrecision(12));
+ nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
if (tick<maxValue) {
var highlightColor = tinycolor(keyColor);
var percent = (tick-minValue)/total;
@@ -86,9 +92,8 @@ export default class TbAnalogueRadialGauge {
//borderShadowWidth: (settings.showBorder !== false) ? 3 : 0,
// number formats
- valueInt: settings.valueInt || 3,
- valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null)
- ? settings.valueDec : 2,
+ valueInt: valueInt,
+ valueDec: valueDec,
majorTicksInt: 1,
majorTicksDec: 0,
ui/src/locale/en_US.json 25(+22 -3)
diff --git a/ui/src/locale/en_US.json b/ui/src/locale/en_US.json
index 5de8938..ccbc3e7 100644
--- a/ui/src/locale/en_US.json
+++ b/ui/src/locale/en_US.json
@@ -40,7 +40,9 @@
"refresh": "Refresh",
"undo": "Undo",
"copy": "Copy",
- "paste": "Paste"
+ "paste": "Paste",
+ "import": "Import",
+ "export": "Export"
},
"admin": {
"general": "General",
@@ -214,7 +216,19 @@
"vertical-margin-required": "Vertical margin value is required.",
"min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.",
"max-vertical-margin-message": "Only 50 is allowed as maximum vertical margin value.",
- "display-title": "Display dashboard title"
+ "display-title": "Display dashboard title",
+ "import": "Import dashboard",
+ "export": "Export dashboard",
+ "export-failed-error": "Unable to export dashboard: {error}",
+ "create-new-dashboard": "Create new dashboard",
+ "dashboard-file": "Dashboard file",
+ "invalid-dashboard-file-error": "Unable to import dashboard: Invalid dashboard data structure.",
+ "dashboard-import-missing-aliases-title": "Select missing devices for dashboard aliases",
+ "create-new-widget": "Create new widget",
+ "import-widget": "Import widget",
+ "widget-file": "Widget file",
+ "invalid-widget-file-error": "Unable to import widget: Invalid widget data structure.",
+ "widget-import-missing-aliases-title": "Select missing devices used by widget"
},
"datakey": {
"settings": "Settings",
@@ -370,6 +384,10 @@
"avatar": "Avatar",
"open-user-menu": "Open user menu"
},
+ "import": {
+ "no-file": "No file selected",
+ "drop-file": "Drop a JSON file or click to select a file to upload."
+ },
"item": {
"selected": "Selected"
},
@@ -612,7 +630,8 @@
"widget-type-load-failed-error": "Failed to load widget type!",
"widget-template-load-failed-error": "Failed to load widget template!",
"add": "Add Widget",
- "undo": "Undo widget changes"
+ "undo": "Undo widget changes",
+ "export": "Export widget"
},
"widgets-bundle": {
"current": "Current bundle",