Browse Source

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

Import/Export. Bug fixes.
pull/30/merge
Andrew Shvayka 10 years ago
committed by GitHub
parent
commit
1305015585
  1. 1
      ui/package.json
  2. 4
      ui/src/app/api/device.service.js
  3. 13
      ui/src/app/components/dashboard.directive.js
  4. 12
      ui/src/app/components/dashboard.tpl.html
  5. 4
      ui/src/app/components/grid.directive.js
  6. 23
      ui/src/app/components/grid.tpl.html
  7. 10
      ui/src/app/components/react/json-form-number.jsx
  8. 10
      ui/src/app/dashboard/add-widget.controller.js
  9. 1
      ui/src/app/dashboard/dashboard-fieldset.tpl.html
  10. 96
      ui/src/app/dashboard/dashboard.controller.js
  11. 1
      ui/src/app/dashboard/dashboard.directive.js
  12. 43
      ui/src/app/dashboard/dashboard.tpl.html
  13. 38
      ui/src/app/dashboard/dashboards.controller.js
  14. 1
      ui/src/app/dashboard/dashboards.tpl.html
  15. 55
      ui/src/app/dashboard/device-aliases.controller.js
  16. 6
      ui/src/app/dashboard/device-aliases.tpl.html
  17. 10
      ui/src/app/dashboard/edit-widget.directive.js
  18. 2
      ui/src/app/dashboard/index.js
  19. 11
      ui/src/app/global-interceptor.service.js
  20. 71
      ui/src/app/import-export/import-dialog.controller.js
  21. 70
      ui/src/app/import-export/import-dialog.scss
  22. 72
      ui/src/app/import-export/import-dialog.tpl.html
  23. 373
      ui/src/app/import-export/import-export.service.js
  24. 24
      ui/src/app/import-export/index.js
  25. 56
      ui/src/app/services/item-buffer.service.js
  26. 15
      ui/src/app/widget/lib/analogue-linear-gauge.js
  27. 15
      ui/src/app/widget/lib/analogue-radial-gauge.js
  28. 25
      ui/src/locale/en_US.json

1
ui/package.json

@ -54,6 +54,7 @@
"json-schema-defaults": "^0.2.0", "json-schema-defaults": "^0.2.0",
"justgage": "^1.2.2", "justgage": "^1.2.2",
"material-ui": "^0.16.1", "material-ui": "^0.16.1",
"material-ui-number-input": "^5.0.16",
"md-color-picker": "^0.2.6", "md-color-picker": "^0.2.6",
"mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5", "mdPickers": "git://github.com/alenaksu/mdPickers.git#0.7.5",
"moment": "^2.15.0", "moment": "^2.15.0",

4
ui/src/app/api/device.service.js

@ -88,10 +88,10 @@ function DeviceService($http, $q, $filter, telemetryWebsocketService, types) {
return deferred.promise; return deferred.promise;
} }
function getDevice(deviceId) { function getDevice(deviceId, ignoreErrors) {
var deferred = $q.defer(); var deferred = $q.defer();
var url = '/api/device/' + deviceId; 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); deferred.resolve(response.data);
}, function fail(response) { }, function fail(response) {
deferred.reject(response.data); deferred.reject(response.data);

13
ui/src/app/components/dashboard.directive.js

@ -58,8 +58,10 @@ function Dashboard() {
isMobile: '=', isMobile: '=',
isMobileDisabled: '=?', isMobileDisabled: '=?',
isEditActionEnabled: '=', isEditActionEnabled: '=',
isExportActionEnabled: '=',
isRemoveActionEnabled: '=', isRemoveActionEnabled: '=',
onEditWidget: '&?', onEditWidget: '&?',
onExportWidget: '&?',
onRemoveWidget: '&?', onRemoveWidget: '&?',
onWidgetMouseDown: '&?', onWidgetMouseDown: '&?',
onWidgetClicked: '&?', onWidgetClicked: '&?',
@ -139,6 +141,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $log, toast
vm.showWidgetTitle = showWidgetTitle; vm.showWidgetTitle = showWidgetTitle;
vm.hasTimewindow = hasTimewindow; vm.hasTimewindow = hasTimewindow;
vm.editWidget = editWidget; vm.editWidget = editWidget;
vm.exportWidget = exportWidget;
vm.removeWidget = removeWidget; vm.removeWidget = removeWidget;
vm.loading = loading; 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) { function removeWidget($event, widget) {
resetWidgetClick(); resetWidgetClick();
if ($event) { if ($event) {

12
ui/src/app/components/dashboard.tpl.html

@ -62,6 +62,18 @@
edit edit
</md-icon> </md-icon>
</md-button> </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" <md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()" ng-disabled="vm.loading()"
class="md-icon-button md-primary" class="md-icon-button md-primary"

4
ui/src/app/components/grid.directive.js

@ -327,6 +327,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
icon: "add" icon: "add"
}; };
vm.addItemActionsOpen = false;
vm.addItemActions = vm.config.addItemActions || [];
vm.onGridInited = vm.config.onGridInited || function () { vm.onGridInited = vm.config.onGridInited || function () {
}; };

23
ui/src/app/components/grid.tpl.html

@ -79,7 +79,7 @@
</tb-details-sidenav> </tb-details-sidenav>
</section> </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" <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() }}"> ng-click="groupAction.onAction($event, vm.items)" aria-label="{{ groupAction.name() }}">
<md-tooltip md-direction="top"> <md-tooltip md-direction="top">
@ -93,10 +93,29 @@
</md-tooltip> </md-tooltip>
<ng-md-icon icon="arrow_drop_up"></ng-md-icon> <ng-md-icon icon="arrow_drop_up"></ng-md-icon>
</md-button> </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"> <md-tooltip md-direction="top">
{{ vm.addItemAction.details() }} {{ vm.addItemAction.details() }}
</md-tooltip> </md-tooltip>
<ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon> <ng-md-icon icon="{{ vm.addItemAction.icon }}"></ng-md-icon>
</md-button> </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> </section>

10
ui/src/app/components/react/json-form-number.jsx

@ -15,7 +15,7 @@
*/ */
import React from 'react'; import React from 'react';
import ThingsboardBaseComponent from './json-form-base-component.jsx'; 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 { class ThingsboardNumber extends React.Component {
@ -63,16 +63,18 @@ class ThingsboardNumber extends React.Component {
if (this.state.focused) { if (this.state.focused) {
fieldClass += " tb-focused"; fieldClass += " tb-focused";
} }
var value = this.state.lastSuccessfulValue;
value = Number(value);
return ( return (
<TextField <NumberInput
className={fieldClass} className={fieldClass}
type={this.props.form.type} strategy="allow"
floatingLabelText={this.props.form.title} floatingLabelText={this.props.form.title}
hintText={this.props.form.placeholder} hintText={this.props.form.placeholder}
errorText={this.props.error} errorText={this.props.error}
onChange={this.preValidationCheck} onChange={this.preValidationCheck}
defaultValue={this.state.lastSuccessfulValue} defaultValue={value}
ref="numberField" ref="numberField"
disabled={this.props.form.readonly} disabled={this.props.form.readonly}
onFocus={this.onFocus} onFocus={this.onFocus}

10
ui/src/app/dashboard/add-widget.controller.js

@ -102,10 +102,12 @@ export default function AddWidgetController($scope, widgetService, deviceService
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: deviceAliasesTemplate, templateUrl: deviceAliasesTemplate,
locals: { locals: {
deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases), config: {
aliasToWidgetsMap: null, deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
isSingleDevice: true, widgets: null,
singleDeviceAlias: singleDeviceAlias isSingleDevice: true,
singleDeviceAlias: singleDeviceAlias
}
}, },
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
fullscreen: true, fullscreen: true,

1
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="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="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-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"> <md-content class="md-padding" layout="column">

96
ui/src/app/dashboard/dashboard.controller.js

@ -23,7 +23,7 @@ import addWidgetTemplate from './add-widget.tpl.html';
/*@ngInject*/ /*@ngInject*/
export default function DashboardController(types, widgetService, userService, 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) { $scope, $state, $stateParams, $mdDialog, $timeout, $document, $q, $translate, $filter) {
var user = userService.getCurrentUser(); var user = userService.getCurrentUser();
@ -53,6 +53,8 @@ export default function DashboardController(types, widgetService, userService,
vm.prepareDashboardContextMenu = prepareDashboardContextMenu; vm.prepareDashboardContextMenu = prepareDashboardContextMenu;
vm.prepareWidgetContextMenu = prepareWidgetContextMenu; vm.prepareWidgetContextMenu = prepareWidgetContextMenu;
vm.editWidget = editWidget; vm.editWidget = editWidget;
vm.exportWidget = exportWidget;
vm.importWidget = importWidget;
vm.isTenantAdmin = isTenantAdmin; vm.isTenantAdmin = isTenantAdmin;
vm.loadDashboard = loadDashboard; vm.loadDashboard = loadDashboard;
vm.noData = noData; vm.noData = noData;
@ -210,44 +212,17 @@ export default function DashboardController(types, widgetService, userService,
} }
function openDeviceAliases($event) { 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({ $mdDialog.show({
controller: 'DeviceAliasesController', controller: 'DeviceAliasesController',
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: deviceAliasesTemplate, templateUrl: deviceAliasesTemplate,
locals: { locals: {
deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases), config: {
aliasToWidgetsMap: aliasToWidgetsMap, deviceAliases: angular.copy(vm.dashboard.configuration.deviceAliases),
isSingleDevice: false, widgets: vm.widgets,
singleDeviceAlias: null isSingleDevice: false,
singleDeviceAlias: null
}
}, },
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
skipHide: true, 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) { function widgetMouseDown($event, widget) {
if (vm.isEdit && !vm.isEditingWidget) { if (vm.isEdit && !vm.isEditingWidget) {
vm.dashboardContainer.selectWidget(widget, 0); vm.dashboardContainer.selectWidget(widget, 0);
@ -438,48 +423,7 @@ export default function DashboardController(types, widgetService, userService,
} }
function copyWidget($event, widget) { function copyWidget($event, widget) {
var aliasesInfo = { itembuffer.copyWidget(vm.dashboard, widget);
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);
} }
function helpLinkIdForWidgetType() { function helpLinkIdForWidgetType() {

1
ui/src/app/dashboard/dashboard.directive.js

@ -36,6 +36,7 @@ export default function DashboardDirective($compile, $templateCache) {
theForm: '=', theForm: '=',
onAssignToCustomer: '&', onAssignToCustomer: '&',
onUnassignFromCustomer: '&', onUnassignFromCustomer: '&',
onExportDashboard: '&',
onDeleteDashboard: '&' onDeleteDashboard: '&'
} }
}; };

43
ui/src/app/dashboard/dashboard.tpl.html

@ -80,8 +80,10 @@
is-mobile="vm.forceDashboardMobileMode" is-mobile="vm.forceDashboardMobileMode"
is-mobile-disabled="vm.widgetEditMode" is-mobile-disabled="vm.widgetEditMode"
is-edit-action-enabled="vm.isEdit || 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" is-remove-action-enabled="vm.isEdit && !vm.widgetEditMode"
on-edit-widget="vm.editWidget(event, widget)" on-edit-widget="vm.editWidget(event, widget)"
on-export-widget="vm.exportWidget(event, widget)"
on-widget-mouse-down="vm.widgetMouseDown(event, widget)" on-widget-mouse-down="vm.widgetMouseDown(event, widget)"
on-widget-clicked="vm.widgetClicked(event, widget)" on-widget-clicked="vm.widgetClicked(event, widget)"
on-widget-context-menu="vm.widgetContextMenu(event, widget)" on-widget-context-menu="vm.widgetContextMenu(event, widget)"
@ -180,15 +182,38 @@
</div> </div>
</tb-details-sidenav> </tb-details-sidenav>
<!-- </section> --> <!-- </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.isAddingWidget && vm.isEdit && !vm.widgetEditMode" <md-fab-speed-dial 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)" md-open="vm.addItemActionsOpen" class="md-scale" md-direction="up">
aria-label="{{ 'dashboard.add-widget' | translate }}"> <md-fab-trigger>
<md-tooltip md-direction="top"> <md-button ng-disabled="loading"
{{ 'dashboard.add-widget' | translate }} class="tb-btn-footer md-accent md-hue-2 md-fab"
</md-tooltip> aria-label="{{ 'dashboard.add-widget' | translate }}">
<ng-md-icon icon="add"></ng-md-icon> <md-tooltip md-direction="top">
</md-button> {{ '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" <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" class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.apply' | translate }}" aria-label="{{ 'action.apply' | translate }}"

38
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 */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@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; var customerId = $stateParams.customerId;
@ -86,6 +86,7 @@ export default function DashboardsController(userService, dashboardService, cust
vm.assignToCustomer = assignToCustomer; vm.assignToCustomer = assignToCustomer;
vm.unassignFromCustomer = unassignFromCustomer; vm.unassignFromCustomer = unassignFromCustomer;
vm.exportDashboard = exportDashboard;
initController(); initController();
@ -113,6 +114,14 @@ export default function DashboardsController(userService, dashboardService, cust
}; };
dashboardActionsList.push( 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) { onAction: function ($event, item) {
assignToCustomer($event, [ item.id.id ]); assignToCustomer($event, [ item.id.id ]);
@ -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') { } else if (vm.dashboardsScope === 'customer' || vm.dashboardsScope === 'customer_user') {
fetchDashboardsFunction = function (pageLink) { fetchDashboardsFunction = function (pageLink) {
return dashboardService.getCustomerDashboards(customerId, 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) { function unassignDashboardsFromCustomer($event, items) {
var confirm = $mdDialog.confirm() var confirm = $mdDialog.confirm()
.targetEvent($event) .targetEvent($event)

1
ui/src/app/dashboard/dashboards.tpl.html

@ -25,5 +25,6 @@
the-form="vm.grid.detailsForm" the-form="vm.grid.detailsForm"
on-assign-to-customer="vm.assignToCustomer(event, [ vm.grid.detailsConfig.currentItem.id.id ])" 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-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> on-delete-dashboard="vm.grid.deleteItem(event, vm.grid.detailsConfig.currentItem)"></tb-dashboard-details>
</tb-grid> </tb-grid>

55
ui/src/app/dashboard/device-aliases.controller.js

@ -17,16 +17,18 @@ import './device-aliases.scss';
/*@ngInject*/ /*@ngInject*/
export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate, export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
deviceAliases, aliasToWidgetsMap, isSingleDevice, singleDeviceAlias) { types, config) {
var vm = this; var vm = this;
vm.isSingleDevice = isSingleDevice; vm.isSingleDevice = config.isSingleDevice;
vm.singleDeviceAlias = singleDeviceAlias; vm.singleDeviceAlias = config.singleDeviceAlias;
vm.deviceAliases = []; vm.deviceAliases = [];
vm.aliasToWidgetsMap = aliasToWidgetsMap;
vm.singleDevice = null; vm.singleDevice = null;
vm.singleDeviceSearchText = ''; vm.singleDeviceSearchText = '';
vm.title = config.customTitle ? config.customTitle : 'device.aliases';
vm.disableAdd = config.disableAdd;
vm.aliasToWidgetsMap = {};
vm.addAlias = addAlias; vm.addAlias = addAlias;
vm.cancel = cancel; vm.cancel = cancel;
@ -39,9 +41,48 @@ export default function DeviceAliasesController(deviceService, toast, $scope, $m
initController(); initController();
function initController() { function initController() {
for (var aliasId in deviceAliases) { var aliasId;
var alias = deviceAliases[aliasId].alias; if (config.widgets) {
var deviceId = deviceAliases[aliasId].deviceId; 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: ''}; var deviceAlias = {id: aliasId, alias: alias, device: null, changed: false, searchText: ''};
if (deviceId) { if (deviceId) {
fetchAliasDevice(deviceAlias, deviceId); fetchAliasDevice(deviceAlias, deviceId);

6
ui/src/app/dashboard/device-aliases.tpl.html

@ -15,11 +15,11 @@
limitations under the License. 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()"> <form name="theForm" ng-submit="vm.save()">
<md-toolbar> <md-toolbar>
<div class="md-toolbar-tools"> <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> <span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()"> <md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon> <ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
@ -109,7 +109,7 @@
</div> </div>
</div> </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-button ng-disabled="loading" class="md-primary md-raised" ng-click="vm.addAlias($event)" aria-label="{{ 'action.add' | translate }}">
<md-tooltip md-direction="top"> <md-tooltip md-direction="top">
{{ 'device.add-alias' | translate }} {{ 'device.add-alias' | translate }}

10
ui/src/app/dashboard/edit-widget.directive.js

@ -76,10 +76,12 @@ export default function EditWidgetDirective($compile, $templateCache, widgetServ
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: deviceAliasesTemplate, templateUrl: deviceAliasesTemplate,
locals: { locals: {
deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases), config: {
aliasToWidgetsMap: null, deviceAliases: angular.copy(scope.dashboard.configuration.deviceAliases),
isSingleDevice: true, widgets: null,
singleDeviceAlias: singleDeviceAlias isSingleDevice: true,
singleDeviceAlias: singleDeviceAlias
}
}, },
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
fullscreen: true, fullscreen: true,

2
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 thingsboardWidgetsBundleSelect from '../components/widgets-bundle-select.directive';
import thingsboardTypes from '../common/types.constant'; import thingsboardTypes from '../common/types.constant';
import thingsboardItemBuffer from '../services/item-buffer.service'; import thingsboardItemBuffer from '../services/item-buffer.service';
import thingsboardImportExport from '../import-export';
import DashboardRoutes from './dashboard.routes'; import DashboardRoutes from './dashboard.routes';
import DashboardsController from './dashboards.controller'; import DashboardsController from './dashboards.controller';
@ -47,6 +48,7 @@ export default angular.module('thingsboard.dashboard', [
gridster.name, gridster.name,
thingsboardTypes, thingsboardTypes,
thingsboardItemBuffer, thingsboardItemBuffer,
thingsboardImportExport,
thingsboardGrid, thingsboardGrid,
thingsboardApiWidget, thingsboardApiWidget,
thingsboardApiUser, thingsboardApiUser,

11
ui/src/app/global-interceptor.service.js

@ -148,6 +148,7 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
$rootScope.loading = false; $rootScope.loading = false;
} }
var unhandled = false; var unhandled = false;
var ignoreErrors = rejection.config.ignoreErrors;
if (rejection.refreshTokenPending || rejection.status === 401) { if (rejection.refreshTokenPending || rejection.status === 401) {
var errorCode = rejectionErrorCode(rejection); var errorCode = rejectionErrorCode(rejection);
if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) { if (rejection.refreshTokenPending || (errorCode && errorCode === getTypes().serverErrorCode.jwtTokenExpired)) {
@ -156,13 +157,17 @@ export default function GlobalInterceptor($rootScope, $q, $injector) {
unhandled = true; unhandled = true;
} }
} else if (rejection.status === 403) { } else if (rejection.status === 403) {
$rootScope.$broadcast('forbidden'); if (!ignoreErrors) {
$rootScope.$broadcast('forbidden');
}
} else if (rejection.status === 0 || rejection.status === -1) { } else if (rejection.status === 0 || rejection.status === -1) {
getToast().showError(getTranslate().instant('error.unable-to-connect')); getToast().showError(getTranslate().instant('error.unable-to-connect'));
} else if (!rejection.config.url.startsWith('/api/plugins/rpc')) { } else if (!rejection.config.url.startsWith('/api/plugins/rpc')) {
if (rejection.status === 404) { if (rejection.status === 404) {
getToast().showError(rejection.config.method + ": " + rejection.config.url + "<br/>" + if (!ignoreErrors) {
rejection.status + ": " + rejection.statusText); getToast().showError(rejection.config.method + ": " + rejection.config.url + "<br/>" +
rejection.status + ": " + rejection.statusText);
}
} else { } else {
unhandled = true; unhandled = true;
} }

71
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);
}
}

70
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;
}

72
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>

373
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 */

24
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;

56
ui/src/app/services/item-buffer.service.js

@ -25,11 +25,12 @@ export default angular.module('thingsboard.itembuffer', [angularStorage])
.name; .name;
/*@ngInject*/ /*@ngInject*/
function ItemBuffer(bufferStore) { function ItemBuffer(bufferStore, types) {
const WIDGET_ITEM = "widget_item"; const WIDGET_ITEM = "widget_item";
var service = { var service = {
prepareWidgetItem: prepareWidgetItem,
copyWidget: copyWidget, copyWidget: copyWidget,
hasWidget: hasWidget, hasWidget: hasWidget,
pasteWidget: pasteWidget, pasteWidget: pasteWidget,
@ -56,12 +57,57 @@ function ItemBuffer(bufferStore) {
} }
**/ **/
function copyWidget(widget, aliasesInfo, originalColumns) { function prepareWidgetItem(dashboard, widget) {
var widgetItem = { 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, widget: widget,
aliasesInfo: aliasesInfo, aliasesInfo: aliasesInfo,
originalColumns: originalColumns originalColumns: originalColumns
} }
}
function copyWidget(dashboard, widget) {
var widgetItem = prepareWidgetItem(dashboard, widget);
bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem)); bufferStore.set(WIDGET_ITEM, angular.toJson(widgetItem));
} }
@ -69,7 +115,7 @@ function ItemBuffer(bufferStore) {
return bufferStore.get(WIDGET_ITEM); return bufferStore.get(WIDGET_ITEM);
} }
function pasteWidget(targetDasgboard, position) { function pasteWidget(targetDashboard, position) {
var widgetItemJson = bufferStore.get(WIDGET_ITEM); var widgetItemJson = bufferStore.get(WIDGET_ITEM);
if (widgetItemJson) { if (widgetItemJson) {
var widgetItem = angular.fromJson(widgetItemJson); var widgetItem = angular.fromJson(widgetItemJson);
@ -82,7 +128,7 @@ function ItemBuffer(bufferStore) {
targetRow = position.row; targetRow = position.row;
targetColumn = position.column; targetColumn = position.column;
} }
addWidgetToDashboard(targetDasgboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn); addWidgetToDashboard(targetDashboard, widget, aliasesInfo, originalColumns, targetRow, targetColumn);
} }
} }

15
ui/src/app/widget/lib/analogue-linear-gauge.js

@ -34,7 +34,13 @@ export default class TbAnalogueLinearGauge {
var majorTicksCount = settings.majorTicksCount || 10; var majorTicksCount = settings.majorTicksCount || 10;
var total = maxValue-minValue; var total = maxValue-minValue;
var step = (total/majorTicksCount); 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 majorTicks = [];
var highlights = []; var highlights = [];
@ -44,7 +50,7 @@ export default class TbAnalogueLinearGauge {
var majorTick = tick + minValue; var majorTick = tick + minValue;
majorTicks.push(majorTick); majorTicks.push(majorTick);
var nextTick = tick+step; var nextTick = tick+step;
nextTick = parseFloat(parseFloat(nextTick).toPrecision(12)); nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
if (tick<total) { if (tick<total) {
var highlightColor = tinycolor(keyColor); var highlightColor = tinycolor(keyColor);
var percent = tick/total; var percent = tick/total;
@ -89,9 +95,8 @@ export default class TbAnalogueLinearGauge {
// borders // borders
// number formats // number formats
valueInt: settings.valueInt || 3, valueInt: valueInt,
valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null) valueDec: valueDec,
? settings.valueDec : 2,
majorTicksInt: 1, majorTicksInt: 1,
majorTicksDec: 0, majorTicksDec: 0,

15
ui/src/app/widget/lib/analogue-radial-gauge.js

@ -35,7 +35,13 @@ export default class TbAnalogueRadialGauge {
var majorTicksCount = settings.majorTicksCount || 10; var majorTicksCount = settings.majorTicksCount || 10;
var total = maxValue-minValue; var total = maxValue-minValue;
var step = (total/majorTicksCount); 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 majorTicks = [];
var highlights = []; var highlights = [];
@ -44,7 +50,7 @@ export default class TbAnalogueRadialGauge {
while(tick<=maxValue) { while(tick<=maxValue) {
majorTicks.push(tick); majorTicks.push(tick);
var nextTick = tick+step; var nextTick = tick+step;
nextTick = parseFloat(parseFloat(nextTick).toPrecision(12)); nextTick = parseFloat(parseFloat(nextTick).toFixed(valueDec));
if (tick<maxValue) { if (tick<maxValue) {
var highlightColor = tinycolor(keyColor); var highlightColor = tinycolor(keyColor);
var percent = (tick-minValue)/total; var percent = (tick-minValue)/total;
@ -86,9 +92,8 @@ export default class TbAnalogueRadialGauge {
//borderShadowWidth: (settings.showBorder !== false) ? 3 : 0, //borderShadowWidth: (settings.showBorder !== false) ? 3 : 0,
// number formats // number formats
valueInt: settings.valueInt || 3, valueInt: valueInt,
valueDec: (angular.isDefined(settings.valueDec) && settings.valueDec !== null) valueDec: valueDec,
? settings.valueDec : 2,
majorTicksInt: 1, majorTicksInt: 1,
majorTicksDec: 0, majorTicksDec: 0,

25
ui/src/locale/en_US.json

@ -40,7 +40,9 @@
"refresh": "Refresh", "refresh": "Refresh",
"undo": "Undo", "undo": "Undo",
"copy": "Copy", "copy": "Copy",
"paste": "Paste" "paste": "Paste",
"import": "Import",
"export": "Export"
}, },
"admin": { "admin": {
"general": "General", "general": "General",
@ -214,7 +216,19 @@
"vertical-margin-required": "Vertical margin value is required.", "vertical-margin-required": "Vertical margin value is required.",
"min-vertical-margin-message": "Only 0 is allowed as minimum vertical margin value.", "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.", "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": { "datakey": {
"settings": "Settings", "settings": "Settings",
@ -370,6 +384,10 @@
"avatar": "Avatar", "avatar": "Avatar",
"open-user-menu": "Open user menu" "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": { "item": {
"selected": "Selected" "selected": "Selected"
}, },
@ -612,7 +630,8 @@
"widget-type-load-failed-error": "Failed to load widget type!", "widget-type-load-failed-error": "Failed to load widget type!",
"widget-template-load-failed-error": "Failed to load widget template!", "widget-template-load-failed-error": "Failed to load widget template!",
"add": "Add Widget", "add": "Add Widget",
"undo": "Undo widget changes" "undo": "Undo widget changes",
"export": "Export widget"
}, },
"widgets-bundle": { "widgets-bundle": {
"current": "Current bundle", "current": "Current bundle",

Loading…
Cancel
Save