Browse Source

UI: Implement Asset/Device type management.

pull/154/head
Igor Kulikov 10 years ago
parent
commit
90ef91e3a1
  1. 24
      ui/src/app/api/asset.service.js
  2. 24
      ui/src/app/api/device.service.js
  3. 4
      ui/src/app/app.config.js
  4. 7
      ui/src/app/asset/asset-card.tpl.html
  5. 14
      ui/src/app/asset/asset-fieldset.tpl.html
  6. 12
      ui/src/app/asset/asset.controller.js
  7. 1
      ui/src/app/asset/asset.directive.js
  8. 6
      ui/src/app/asset/asset.routes.js
  9. 248
      ui/src/app/components/datasource-device.directive.js
  10. 44
      ui/src/app/components/datasource-device.scss
  11. 137
      ui/src/app/components/datasource-device.tpl.html
  12. 144
      ui/src/app/components/device-alias-select.directive.js
  13. 52
      ui/src/app/components/device-alias-select.tpl.html
  14. 217
      ui/src/app/components/device-filter.directive.js
  15. 45
      ui/src/app/components/device-filter.scss
  16. 67
      ui/src/app/components/device-filter.tpl.html
  17. 28
      ui/src/app/components/grid.directive.js
  18. 9
      ui/src/app/components/grid.tpl.html
  19. 32
      ui/src/app/dashboard/aliases-device-select-button.tpl.html
  20. 31
      ui/src/app/dashboard/aliases-device-select-panel.controller.js
  21. 33
      ui/src/app/dashboard/aliases-device-select-panel.tpl.html
  22. 153
      ui/src/app/dashboard/aliases-device-select.directive.js
  23. 46
      ui/src/app/dashboard/aliases-device-select.scss
  24. 227
      ui/src/app/dashboard/device-aliases.controller.js
  25. 94
      ui/src/app/dashboard/device-aliases.tpl.html
  26. 2
      ui/src/app/dashboard/states/manage-dashboard-states.tpl.html
  27. 50
      ui/src/app/device/attribute/add-attribute-dialog.controller.js
  28. 95
      ui/src/app/device/attribute/add-attribute-dialog.tpl.html
  29. 73
      ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js
  30. 81
      ui/src/app/device/attribute/add-widget-to-dashboard-dialog.tpl.html
  31. 421
      ui/src/app/device/attribute/attribute-table.directive.js
  32. 50
      ui/src/app/device/attribute/attribute-table.scss
  33. 210
      ui/src/app/device/attribute/attribute-table.tpl.html
  34. 71
      ui/src/app/device/attribute/edit-attribute-value.controller.js
  35. 72
      ui/src/app/device/attribute/edit-attribute-value.tpl.html
  36. 7
      ui/src/app/device/device-card.tpl.html
  37. 7
      ui/src/app/device/device-fieldset.tpl.html
  38. 12
      ui/src/app/device/device.controller.js
  39. 1
      ui/src/app/device/device.directive.js
  40. 6
      ui/src/app/device/device.routes.js
  41. 2
      ui/src/app/entity/attribute/attribute-table.tpl.html
  42. 141
      ui/src/app/entity/entity-subtype-autocomplete.directive.js
  43. 8
      ui/src/app/entity/entity-subtype-autocomplete.scss
  44. 41
      ui/src/app/entity/entity-subtype-autocomplete.tpl.html
  45. 138
      ui/src/app/entity/entity-subtype-select.directive.js
  46. 10
      ui/src/app/entity/entity-subtype-select.scss
  47. 28
      ui/src/app/entity/entity-subtype-select.tpl.html
  48. 4
      ui/src/app/entity/index.js
  49. 37
      ui/src/app/layout/home.controller.js
  50. 8
      ui/src/app/layout/home.scss
  51. 18
      ui/src/app/layout/home.tpl.html
  52. 6
      ui/src/app/layout/user-menu.directive.js
  53. 7
      ui/src/app/locale/locale.constant.js

24
ui/src/app/api/asset.service.js

@ -31,7 +31,8 @@ function AssetService($http, $q, customerService, userService) {
getTenantAssets: getTenantAssets,
getCustomerAssets: getCustomerAssets,
findByQuery: findByQuery,
fetchAssetsByNameFilter: fetchAssetsByNameFilter
fetchAssetsByNameFilter: fetchAssetsByNameFilter,
getAssetTypes: getAssetTypes
}
return service;
@ -152,7 +153,7 @@ function AssetService($http, $q, customerService, userService) {
return deferred.promise;
}
function getTenantAssets(pageLink, applyCustomersInfo, config) {
function getTenantAssets(pageLink, applyCustomersInfo, config, type) {
var deferred = $q.defer();
var url = '/api/tenant/assets?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
@ -164,6 +165,9 @@ function AssetService($http, $q, customerService, userService) {
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
if (angular.isDefined(type) && type.length) {
url += '&type=' + type;
}
$http.get(url, config).then(function success(response) {
if (applyCustomersInfo) {
customerService.applyAssignedCustomersInfo(response.data.data).then(
@ -184,7 +188,7 @@ function AssetService($http, $q, customerService, userService) {
return deferred.promise;
}
function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config) {
function getCustomerAssets(customerId, pageLink, applyCustomersInfo, config, type) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/assets?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
@ -196,6 +200,9 @@ function AssetService($http, $q, customerService, userService) {
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
if (angular.isDefined(type) && type.length) {
url += '&type=' + type;
}
$http.get(url, config).then(function success(response) {
if (applyCustomersInfo) {
customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
@ -258,4 +265,15 @@ function AssetService($http, $q, customerService, userService) {
return deferred.promise;
}
function getAssetTypes() {
var deferred = $q.defer();
var url = '/api/asset/types';
$http.get(url).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
}

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

@ -41,12 +41,13 @@ function DeviceService($http, $q, attributeService, customerService, types) {
deleteDeviceAttributes: deleteDeviceAttributes,
sendOneWayRpcCommand: sendOneWayRpcCommand,
sendTwoWayRpcCommand: sendTwoWayRpcCommand,
findByQuery: findByQuery
findByQuery: findByQuery,
getDeviceTypes: getDeviceTypes
}
return service;
function getTenantDevices(pageLink, applyCustomersInfo, config) {
function getTenantDevices(pageLink, applyCustomersInfo, config, type) {
var deferred = $q.defer();
var url = '/api/tenant/devices?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
@ -58,6 +59,9 @@ function DeviceService($http, $q, attributeService, customerService, types) {
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
if (angular.isDefined(type) && type.length) {
url += '&type=' + type;
}
$http.get(url, config).then(function success(response) {
if (applyCustomersInfo) {
customerService.applyAssignedCustomersInfo(response.data.data).then(
@ -78,7 +82,7 @@ function DeviceService($http, $q, attributeService, customerService, types) {
return deferred.promise;
}
function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config) {
function getCustomerDevices(customerId, pageLink, applyCustomersInfo, config, type) {
var deferred = $q.defer();
var url = '/api/customer/' + customerId + '/devices?limit=' + pageLink.limit;
if (angular.isDefined(pageLink.textSearch)) {
@ -90,6 +94,9 @@ function DeviceService($http, $q, attributeService, customerService, types) {
if (angular.isDefined(pageLink.textOffset)) {
url += '&textOffset=' + pageLink.textOffset;
}
if (angular.isDefined(type) && type.length) {
url += '&type=' + type;
}
$http.get(url, config).then(function success(response) {
if (applyCustomersInfo) {
customerService.applyAssignedCustomerInfo(response.data.data, customerId).then(
@ -286,4 +293,15 @@ function DeviceService($http, $q, attributeService, customerService, types) {
return deferred.promise;
}
function getDeviceTypes() {
var deferred = $q.defer();
var url = '/api/device/types';
$http.get(url).then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
}

4
ui/src/app/app.config.js

@ -160,10 +160,6 @@ export default function AppConfig($provide,
indigoTheme();
}
$mdThemingProvider.theme('tb-search-input', 'default')
.primaryPalette('tb-primary')
.backgroundPalette('tb-primary');
$mdThemingProvider.setDefaultTheme('default');
//$mdThemingProvider.alwaysWatchTheme(true);
}

7
ui/src/app/asset/asset-card.tpl.html

@ -15,5 +15,8 @@
limitations under the License.
-->
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
<div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
<div flex layout="column" style="margin-top: -10px;">
<div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'asset.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
<div class="tb-small" ng-show="vm.isPublic()">{{'asset.public' | translate}}</div>
</div>

14
ui/src/app/asset/asset-fieldset.tpl.html

@ -56,13 +56,13 @@
<div translate ng-message="required">asset.name-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>asset.type</label>
<input required name="type" ng-model="asset.type">
<div ng-messages="theForm.name.$error">
<div translate ng-message="required">asset.type-required</div>
</div>
</md-input-container>
<tb-entity-subtype-autocomplete
ng-disabled="loading || !isEdit"
tb-required="true"
the-form="theForm"
ng-model="asset.type"
entity-type="types.entityType.asset">
</tb-entity-subtype-autocomplete>
<md-input-container class="md-block">
<label translate>asset.description</label>
<textarea ng-model="asset.additionalInfo.description" rows="2"></textarea>

12
ui/src/app/asset/asset.controller.js

@ -47,7 +47,8 @@ export function AssetCardController(types) {
/*@ngInject*/
export function AssetController(userService, assetService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
export function AssetController($rootScope, userService, assetService, customerService, $state, $stateParams,
$document, $mdDialog, $q, $translate, types) {
var customerId = $stateParams.customerId;
@ -129,8 +130,8 @@ export function AssetController(userService, assetService, customerService, $sta
}
if (vm.assetsScope === 'tenant') {
fetchAssetsFunction = function (pageLink) {
return assetService.getTenantAssets(pageLink, true);
fetchAssetsFunction = function (pageLink, assetType) {
return assetService.getTenantAssets(pageLink, true, null, assetType);
};
deleteAssetFunction = function (assetId) {
return assetService.deleteAsset(assetId);
@ -229,8 +230,8 @@ export function AssetController(userService, assetService, customerService, $sta
} else if (vm.assetsScope === 'customer' || vm.assetsScope === 'customer_user') {
fetchAssetsFunction = function (pageLink) {
return assetService.getCustomerAssets(customerId, pageLink, true);
fetchAssetsFunction = function (pageLink, assetType) {
return assetService.getCustomerAssets(customerId, pageLink, true, null, assetType);
};
deleteAssetFunction = function (assetId) {
return assetService.unassignAssetFromCustomer(assetId);
@ -333,6 +334,7 @@ export function AssetController(userService, assetService, customerService, $sta
var deferred = $q.defer();
assetService.saveAsset(asset).then(
function success(savedAsset) {
$rootScope.$broadcast('assetSaved');
var assets = [ savedAsset ];
customerService.applyAssignedCustomersInfo(assets).then(
function success(items) {

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

@ -25,6 +25,7 @@ export default function AssetDirective($compile, $templateCache, toast, $transla
var template = $templateCache.get(assetFieldsetTemplate);
element.html(template);
scope.types = types;
scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null;

6
ui/src/app/asset/asset.routes.js

@ -20,7 +20,7 @@ import assetsTemplate from './assets.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function AssetRoutes($stateProvider) {
export default function AssetRoutes($stateProvider, types) {
$stateProvider
.state('home.assets', {
url: '/assets',
@ -37,6 +37,8 @@ export default function AssetRoutes($stateProvider) {
data: {
assetsType: 'tenant',
searchEnabled: true,
searchByEntitySubtype: true,
searchEntityType: types.entityType.asset,
pageTitle: 'asset.assets'
},
ncyBreadcrumb: {
@ -58,6 +60,8 @@ export default function AssetRoutes($stateProvider) {
data: {
assetsType: 'customer',
searchEnabled: true,
searchByEntitySubtype: true,
searchEntityType: types.entityType.asset,
pageTitle: 'customer.assets'
},
ncyBreadcrumb: {

248
ui/src/app/components/datasource-device.directive.js

@ -1,248 +0,0 @@
/*
* Copyright © 2016-2017 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 './datasource-device.scss';
import 'md-color-picker';
import tinycolor from 'tinycolor2';
import $ from 'jquery';
import thingsboardTypes from '../common/types.constant';
import thingsboardDatakeyConfigDialog from './datakey-config-dialog.controller';
import thingsboardTruncate from './truncate.filter';
/* eslint-disable import/no-unresolved, import/default */
import datasourceDeviceTemplate from './datasource-device.tpl.html';
import datakeyConfigDialogTemplate from './datakey-config-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/* eslint-disable angular/angularelement */
export default angular.module('thingsboard.directives.datasourceDevice', [thingsboardTruncate, thingsboardTypes, thingsboardDatakeyConfigDialog])
.directive('tbDatasourceDevice', DatasourceDevice)
.name;
/*@ngInject*/
function DatasourceDevice($compile, $templateCache, $q, $mdDialog, $window, $document, $mdColorPicker, $mdConstant, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(datasourceDeviceTemplate);
element.html(template);
scope.ngModelCtrl = ngModelCtrl;
scope.types = types;
scope.selectedTimeseriesDataKey = null;
scope.timeseriesDataKeySearchText = null;
scope.selectedAttributeDataKey = null;
scope.attributeDataKeySearchText = null;
scope.updateValidity = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var dataValid = angular.isDefined(value) && value != null;
ngModelCtrl.$setValidity('deviceData', dataValid);
if (dataValid) {
ngModelCtrl.$setValidity('deviceAlias',
angular.isDefined(value.deviceAliasId) &&
value.deviceAliasId != null);
ngModelCtrl.$setValidity('deviceKeys',
angular.isDefined(value.dataKeys) &&
value.dataKeys != null &&
value.dataKeys.length > 0);
}
}
};
scope.$watch('deviceAlias', function () {
if (ngModelCtrl.$viewValue) {
if (scope.deviceAlias) {
ngModelCtrl.$viewValue.deviceAliasId = scope.deviceAlias.id;
} else {
ngModelCtrl.$viewValue.deviceAliasId = null;
}
scope.updateValidity();
scope.selectedDeviceAliasChange();
}
});
scope.$watch('timeseriesDataKeys', function () {
if (ngModelCtrl.$viewValue) {
var dataKeys = [];
dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
dataKeys = dataKeys.concat(scope.attributeDataKeys);
ngModelCtrl.$viewValue.dataKeys = dataKeys;
scope.updateValidity();
}
}, true);
scope.$watch('attributeDataKeys', function () {
if (ngModelCtrl.$viewValue) {
var dataKeys = [];
dataKeys = dataKeys.concat(scope.timeseriesDataKeys);
dataKeys = dataKeys.concat(scope.attributeDataKeys);
ngModelCtrl.$viewValue.dataKeys = dataKeys;
scope.updateValidity();
}
}, true);
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var deviceAliasId = ngModelCtrl.$viewValue.deviceAliasId;
if (scope.deviceAliases[deviceAliasId]) {
scope.deviceAlias = {id: deviceAliasId, alias: scope.deviceAliases[deviceAliasId].alias,
deviceId: scope.deviceAliases[deviceAliasId].deviceId};
} else {
scope.deviceAlias = null;
}
var timeseriesDataKeys = [];
var attributeDataKeys = [];
for (var d in ngModelCtrl.$viewValue.dataKeys) {
var dataKey = ngModelCtrl.$viewValue.dataKeys[d];
if (dataKey.type === types.dataKeyType.timeseries) {
timeseriesDataKeys.push(dataKey);
} else if (dataKey.type === types.dataKeyType.attribute) {
attributeDataKeys.push(dataKey);
}
}
scope.timeseriesDataKeys = timeseriesDataKeys;
scope.attributeDataKeys = attributeDataKeys;
}
};
scope.textIsNotEmpty = function(text) {
return (text && text != null && text.length > 0) ? true : false;
}
scope.selectedDeviceAliasChange = function () {
if (!scope.timeseriesDataKeySearchText || scope.timeseriesDataKeySearchText === '') {
scope.timeseriesDataKeySearchText = scope.timeseriesDataKeySearchText === '' ? null : '';
}
if (!scope.attributeDataKeySearchText || scope.attributeDataKeySearchText === '') {
scope.attributeDataKeySearchText = scope.attributeDataKeySearchText === '' ? null : '';
}
};
scope.transformTimeseriesDataKeyChip = function (chip) {
return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries});
};
scope.transformAttributeDataKeyChip = function (chip) {
return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
};
scope.showColorPicker = function (event, dataKey) {
$mdColorPicker.show({
value: dataKey.color,
defaultValue: '#fff',
random: tinycolor.random(),
clickOutsideToClose: false,
hasBackdrop: false,
skipHide: true,
preserveScope: false,
mdColorAlphaChannel: true,
mdColorSpectrum: true,
mdColorSliders: true,
mdColorGenericPalette: false,
mdColorMaterialPalette: true,
mdColorHistory: false,
mdColorDefaultTab: 2,
$event: event
}).then(function (color) {
dataKey.color = color;
ngModelCtrl.$setDirty();
});
}
scope.editDataKey = function (event, dataKey, index) {
$mdDialog.show({
controller: 'DatakeyConfigDialogController',
controllerAs: 'vm',
templateUrl: datakeyConfigDialogTemplate,
locals: {
dataKey: angular.copy(dataKey),
dataKeySettingsSchema: scope.datakeySettingsSchema,
deviceAlias: scope.deviceAlias,
deviceAliases: scope.deviceAliases
},
parent: angular.element($document[0].body),
fullscreen: true,
targetEvent: event,
skipHide: true,
onComplete: function () {
var w = angular.element($window);
w.triggerHandler('resize');
}
}).then(function (dataKey) {
if (dataKey.type === types.dataKeyType.timeseries) {
scope.timeseriesDataKeys[index] = dataKey;
} else if (dataKey.type === types.dataKeyType.attribute) {
scope.attributeDataKeys[index] = dataKey;
}
ngModelCtrl.$setDirty();
}, function () {
});
};
scope.dataKeysSearch = function (searchText, type) {
if (scope.deviceAlias) {
var deferred = $q.defer();
scope.fetchDeviceKeys({deviceAliasId: scope.deviceAlias.id, query: searchText, type: type})
.then(function (dataKeys) {
deferred.resolve(dataKeys);
}, function (e) {
deferred.reject(e);
});
return deferred.promise;
} else {
return $q.when([]);
}
};
scope.createKey = function (event, chipsId) {
var chipsChild = $(chipsId, element)[0].firstElementChild;
var el = angular.element(chipsChild);
var chipBuffer = el.scope().$mdChipsCtrl.getChipBuffer();
event.preventDefault();
event.stopPropagation();
el.scope().$mdChipsCtrl.appendChip(chipBuffer.trim());
el.scope().$mdChipsCtrl.resetChipBuffer();
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
scope: {
widgetType: '=',
deviceAliases: '=',
datakeySettingsSchema: '=',
generateDataKey: '&',
fetchDeviceKeys: '&',
onCreateDeviceAlias: '&'
},
link: linker
};
}
/* eslint-enable angular/angularelement */

44
ui/src/app/components/datasource-device.scss

@ -1,44 +0,0 @@
/**
* Copyright © 2016-2017 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 '../../scss/constants';
.tb-device-alias-autocomplete, .tb-timeseries-datakey-autocomplete, .tb-attribute-datakey-autocomplete {
.tb-not-found {
display: block;
line-height: 1.5;
height: 48px;
.tb-no-entries {
line-height: 48px;
}
}
li {
height: auto !important;
white-space: normal !important;
}
}
tb-datasource-device {
@media (min-width: $layout-breakpoint-gt-sm) {
padding-left: 4px;
padding-right: 4px;
}
tb-device-alias-select {
@media (min-width: $layout-breakpoint-gt-sm) {
width: 200px;
max-width: 200px;
}
}
}

137
ui/src/app/components/datasource-device.tpl.html

@ -1,137 +0,0 @@
<!--
Copyright © 2016-2017 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.
-->
<section flex layout='column' layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
<tb-device-alias-select
tb-required="true"
device-aliases="deviceAliases"
ng-model="deviceAlias"
on-create-device-alias="onCreateDeviceAlias({event: event, alias: alias})">
</tb-device-alias-select>
<section flex layout='column'>
<section flex layout='column' layout-align="center" style="padding-left: 4px;">
<md-chips flex
id="timeseries_datakey_chips"
ng-required="true"
ng-model="timeseriesDataKeys" md-autocomplete-snap
md-transform-chip="transformTimeseriesDataKeyChip($chip)"
md-require-match="false">
<md-autocomplete
md-no-cache="true"
id="timeseries_datakey"
md-selected-item="selectedTimeseriesDataKey"
md-search-text="timeseriesDataKeySearchText"
md-items="item in dataKeysSearch(timeseriesDataKeySearchText, types.dataKeyType.timeseries)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{'datakey.timeseries' | translate }}"
md-menu-class="tb-timeseries-datakey-autocomplete">
<span md-highlight-text="timeseriesDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
<md-not-found>
<div class="tb-not-found">
<div class="tb-no-entries" ng-if="!textIsNotEmpty(timeseriesDataKeySearchText)">
<span translate>device.no-keys-found</span>
</div>
<div ng-if="textIsNotEmpty(timeseriesDataKeySearchText)">
<span translate translate-values='{ key: "{{timeseriesDataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>device.no-key-matching</span>
<span>
<a translate ng-click="createKey($event, '#timeseries_datakey_chips')">device.create-new-key</a>
</span>
</div>
</div>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<div layout="row" layout-align="start center" class="tb-attribute-chip">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
<div layout="row" flex>
<div class="tb-chip-label">
{{$chip.label}}
</div>
<div class="tb-chip-separator">: </div>
<div class="tb-chip-label">
<strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
<strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
</div>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
</md-button>
</div>
</md-chip-template>
</md-chips>
<md-chips flex ng-if="widgetType === types.widgetType.latest.value"
id="attribute_datakey_chips"
ng-required="true"
ng-model="attributeDataKeys" md-autocomplete-snap
md-transform-chip="transformAttributeDataKeyChip($chip)"
md-require-match="false">
<md-autocomplete
md-no-cache="true"
id="attribute_datakey"
md-selected-item="selectedAttributeDataKey"
md-search-text="attributeDataKeySearchText"
md-items="item in dataKeysSearch(attributeDataKeySearchText, types.dataKeyType.attribute)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{'datakey.attributes' | translate }}"
md-menu-class="tb-attribute-datakey-autocomplete">
<span md-highlight-text="attributeDataKeySearchText" md-highlight-flags="^i">{{item}}</span>
<md-not-found>
<div class="tb-not-found">
<div class="tb-no-entries" ng-if="!textIsNotEmpty(attributeDataKeySearchText)">
<span translate>device.no-keys-found</span>
</div>
<div ng-if="textIsNotEmpty(attributeDataKeySearchText)">
<span translate translate-values='{ key: "{{attributeDataKeySearchText | truncate:true:6:&apos;...&apos;}}" }'>device.no-key-matching</span>
<span>
<a translate ng-click="createKey($event, '#attribute_datakey_chips')">device.create-new-key</a>
</span>
</div>
</div>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<div layout="row" layout-align="start center" class="tb-attribute-chip">
<div class="tb-color-preview" ng-click="showColorPicker($event, $chip, $index)" style="margin-right: 5px;">
<div class="tb-color-result" ng-style="{background: $chip.color}"></div>
</div>
<div layout="row" flex>
<div class="tb-chip-label">
{{$chip.label}}
</div>
<div class="tb-chip-separator">: </div>
<div class="tb-chip-label">
<strong ng-if="!$chip.postFuncBody">{{$chip.name}}</strong>
<strong ng-if="$chip.postFuncBody">f({{$chip.name}})</strong>
</div>
</div>
<md-button ng-click="editDataKey($event, $chip, $index)" class="md-icon-button tb-md-32">
<md-icon aria-label="edit" class="material-icons tb-md-20">edit</md-icon>
</md-button>
</div>
</md-chip-template>
</md-chips>
</section>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
<div translate ng-message="deviceKeys" ng-if="widgetType === types.widgetType.timeseries.value" class="tb-error-message">datakey.timeseries-required</div>
<div translate ng-message="deviceKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
</div>
</section>
</section>

144
ui/src/app/components/device-alias-select.directive.js

@ -1,144 +0,0 @@
/*
* Copyright © 2016-2017 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 $ from 'jquery';
import './device-alias-select.scss';
/* eslint-disable import/no-unresolved, import/default */
import deviceAliasSelectTemplate from './device-alias-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/* eslint-disable angular/angularelement */
export default angular.module('thingsboard.directives.deviceAliasSelect', [])
.directive('tbDeviceAliasSelect', DeviceAliasSelect)
.name;
/*@ngInject*/
function DeviceAliasSelect($compile, $templateCache, $mdConstant) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(deviceAliasSelectTemplate);
element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
scope.ngModelCtrl = ngModelCtrl;
scope.deviceAliasList = [];
scope.deviceAlias = null;
scope.updateValidity = function () {
var value = ngModelCtrl.$viewValue;
var valid = angular.isDefined(value) && value != null || !scope.tbRequired;
ngModelCtrl.$setValidity('deviceAlias', valid);
};
scope.$watch('deviceAliases', function () {
scope.deviceAliasList = [];
for (var aliasId in scope.deviceAliases) {
var deviceAlias = {id: aliasId, alias: scope.deviceAliases[aliasId].alias, deviceId: scope.deviceAliases[aliasId].deviceId};
scope.deviceAliasList.push(deviceAlias);
}
}, true);
scope.$watch('deviceAlias', function () {
scope.updateView();
});
scope.deviceAliasSearch = function (deviceAliasSearchText) {
return deviceAliasSearchText ? scope.deviceAliasList.filter(
scope.createFilterForDeviceAlias(deviceAliasSearchText)) : scope.deviceAliasList;
};
scope.createFilterForDeviceAlias = function (query) {
var lowercaseQuery = angular.lowercase(query);
return function filterFn(deviceAlias) {
return (angular.lowercase(deviceAlias.alias).indexOf(lowercaseQuery) === 0);
};
};
scope.updateView = function () {
ngModelCtrl.$setViewValue(scope.deviceAlias);
scope.updateValidity();
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
scope.deviceAlias = ngModelCtrl.$viewValue;
}
}
scope.textIsNotEmpty = function(text) {
return (text && text != null && text.length > 0) ? true : false;
}
scope.deviceAliasEnter = function($event) {
if ($event.keyCode === $mdConstant.KEY_CODE.ENTER) {
$event.preventDefault();
if (!scope.deviceAlias) {
var found = scope.deviceAliasSearch(scope.deviceAliasSearchText);
found = found.length > 0;
if (!found) {
scope.createDeviceAlias($event, scope.deviceAliasSearchText);
}
}
}
}
scope.createDeviceAlias = function (event, alias) {
var autoChild = $('#device-autocomplete', element)[0].firstElementChild;
var el = angular.element(autoChild);
el.scope().$mdAutocompleteCtrl.hidden = true;
el.scope().$mdAutocompleteCtrl.hasNotFound = false;
event.preventDefault();
var promise = scope.onCreateDeviceAlias({event: event, alias: alias});
if (promise) {
promise.then(
function success(newAlias) {
el.scope().$mdAutocompleteCtrl.hasNotFound = true;
if (newAlias) {
scope.deviceAliasList.push(newAlias);
scope.deviceAlias = newAlias;
}
},
function fail() {
el.scope().$mdAutocompleteCtrl.hasNotFound = true;
}
);
} else {
el.scope().$mdAutocompleteCtrl.hasNotFound = true;
}
};
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
tbRequired: '=?',
deviceAliases: '=',
onCreateDeviceAlias: '&'
}
};
}
/* eslint-enable angular/angularelement */

52
ui/src/app/components/device-alias-select.tpl.html

@ -1,52 +0,0 @@
<!--
Copyright © 2016-2017 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.
-->
<section layout='column'>
<md-autocomplete id="device-autocomplete"
md-input-name="device_alias"
ng-required="tbRequired"
ng-model="deviceAlias"
md-selected-item="deviceAlias"
md-search-text="deviceAliasSearchText"
md-items="item in deviceAliasSearch(deviceAliasSearchText)"
md-item-text="item.alias"
tb-keydown="deviceAliasEnter($event)"
tb-keypress="deviceAliasEnter($event)"
md-min-length="0"
placeholder="{{ 'device.device-alias' | translate }}"
md-menu-class="tb-device-alias-autocomplete">
<md-item-template>
<span md-highlight-text="deviceAliasSearchText" md-highlight-flags="^i">{{item.alias}}</span>
</md-item-template>
<md-not-found>
<div class="tb-not-found">
<div class="tb-no-entries" ng-if="!textIsNotEmpty(deviceAliasSearchText)">
<span translate>device.no-aliases-found</span>
</div>
<div ng-if="textIsNotEmpty(deviceAliasSearchText)">
<span translate translate-values='{ alias: "{{deviceAliasSearchText | truncate:true:6:&apos;...&apos;}}" }'>device.no-alias-matching</span>
<span>
<a translate ng-click="createDeviceAlias($event, deviceAliasSearchText)">device.create-new-alias</a>
</span>
</div>
</div>
</md-not-found>
</md-autocomplete>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
<div translate ng-message="deviceAlias" class="tb-error-message">device.alias-required</div>
</div>
</section>

217
ui/src/app/components/device-filter.directive.js

@ -1,217 +0,0 @@
/*
* Copyright © 2016-2017 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 deviceFilterTemplate from './device-filter.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import './device-filter.scss';
export default angular.module('thingsboard.directives.deviceFilter', [])
.directive('tbDeviceFilter', DeviceFilter)
.name;
/*@ngInject*/
function DeviceFilter($compile, $templateCache, $q, deviceService) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(deviceFilterTemplate);
element.html(template);
scope.ngModelCtrl = ngModelCtrl;
scope.fetchDevices = function(searchText, limit) {
var pageLink = {limit: limit, textSearch: searchText};
var deferred = $q.defer();
deviceService.getTenantDevices(pageLink, false).then(function success(result) {
deferred.resolve(result.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
scope.updateValidity = function() {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var valid;
if (value.useFilter) {
ngModelCtrl.$setValidity('deviceList', true);
if (angular.isDefined(value.deviceNameFilter) && value.deviceNameFilter.length > 0) {
ngModelCtrl.$setValidity('deviceNameFilter', true);
valid = angular.isDefined(scope.model.matchingFilterDevice) && scope.model.matchingFilterDevice != null;
ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', valid);
} else {
ngModelCtrl.$setValidity('deviceNameFilter', false);
}
} else {
ngModelCtrl.$setValidity('deviceNameFilter', true);
ngModelCtrl.$setValidity('deviceNameFilterDeviceMatch', true);
valid = angular.isDefined(value.deviceList) && value.deviceList.length > 0;
ngModelCtrl.$setValidity('deviceList', valid);
}
}
}
ngModelCtrl.$render = function () {
destroyWatchers();
scope.model = {
useFilter: false,
deviceList: [],
deviceNameFilter: ''
}
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
var model = scope.model;
model.useFilter = value.useFilter === true ? true: false;
model.deviceList = [];
model.deviceNameFilter = value.deviceNameFilter || '';
processDeviceNameFilter(model.deviceNameFilter).then(
function(device) {
scope.model.matchingFilterDevice = device;
if (value.deviceList && value.deviceList.length > 0) {
deviceService.getDevices(value.deviceList).then(function (devices) {
model.deviceList = devices;
updateMatchingDevice();
initWatchers();
});
} else {
updateMatchingDevice();
initWatchers();
}
}
)
}
}
function updateMatchingDevice() {
if (scope.model.useFilter) {
scope.model.matchingDevice = scope.model.matchingFilterDevice;
} else {
if (scope.model.deviceList && scope.model.deviceList.length > 0) {
scope.model.matchingDevice = scope.model.deviceList[0];
} else {
scope.model.matchingDevice = null;
}
}
}
function processDeviceNameFilter(deviceNameFilter) {
var deferred = $q.defer();
if (angular.isDefined(deviceNameFilter) && deviceNameFilter.length > 0) {
scope.fetchDevices(deviceNameFilter, 1).then(function (devices) {
if (devices && devices.length > 0) {
deferred.resolve(devices[0]);
} else {
deferred.resolve(null);
}
});
} else {
deferred.resolve(null);
}
return deferred.promise;
}
function destroyWatchers() {
if (scope.deviceListDeregistration) {
scope.deviceListDeregistration();
scope.deviceListDeregistration = null;
}
if (scope.useFilterDeregistration) {
scope.useFilterDeregistration();
scope.useFilterDeregistration = null;
}
if (scope.deviceNameFilterDeregistration) {
scope.deviceNameFilterDeregistration();
scope.deviceNameFilterDeregistration = null;
}
if (scope.matchingDeviceDeregistration) {
scope.matchingDeviceDeregistration();
scope.matchingDeviceDeregistration = null;
}
}
function initWatchers() {
scope.deviceListDeregistration = scope.$watch('model.deviceList', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
value.deviceList = [];
if (scope.model.deviceList && scope.model.deviceList.length > 0) {
for (var i in scope.model.deviceList) {
value.deviceList.push(scope.model.deviceList[i].id.id);
}
}
updateMatchingDevice();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
}, true);
scope.useFilterDeregistration = scope.$watch('model.useFilter', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
value.useFilter = scope.model.useFilter;
updateMatchingDevice();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
});
scope.deviceNameFilterDeregistration = scope.$watch('model.deviceNameFilter', function (newNameFilter, prevNameFilter) {
if (ngModelCtrl.$viewValue) {
if (!angular.equals(newNameFilter, prevNameFilter)) {
var value = ngModelCtrl.$viewValue;
value.deviceNameFilter = scope.model.deviceNameFilter;
processDeviceNameFilter(value.deviceNameFilter).then(
function(device) {
scope.model.matchingFilterDevice = device;
updateMatchingDevice();
ngModelCtrl.$setViewValue(value);
scope.updateValidity();
}
);
}
}
});
scope.matchingDeviceDeregistration = scope.$watch('model.matchingDevice', function (newMatchingDevice, prevMatchingDevice) {
if (!angular.equals(newMatchingDevice, prevMatchingDevice)) {
if (scope.onMatchingDeviceChange) {
scope.onMatchingDeviceChange({device: newMatchingDevice});
}
}
});
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
isEdit: '=',
onMatchingDeviceChange: '&'
}
};
}

45
ui/src/app/components/device-filter.scss

@ -1,45 +0,0 @@
/**
* Copyright © 2016-2017 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.
*/
.tb-device-filter {
#device_list_chips {
.md-chips {
padding-bottom: 1px;
}
}
.device-name-filter-input {
margin-top: 10px;
margin-bottom: 0px;
.md-errors-spacer {
min-height: 0px;
}
}
.tb-filter-switch {
padding-left: 10px;
.filter-switch {
margin: 0;
}
.filter-label {
margin: 5px 0;
}
}
.tb-error-messages {
margin-top: -11px;
height: 35px;
.tb-error-message {
padding-left: 1px;
}
}
}

67
ui/src/app/components/device-filter.tpl.html

@ -1,67 +0,0 @@
<!--
Copyright © 2016-2017 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.
-->
<section layout='column' class="tb-device-filter">
<section layout='row'>
<section layout="column" flex ng-show="!model.useFilter">
<md-chips flex
id="device_list_chips"
ng-required="!useFilter"
ng-model="model.deviceList" md-autocomplete-snap
md-require-match="true">
<md-autocomplete
md-no-cache="true"
id="device"
md-selected-item="selectedDevice"
md-search-text="deviceSearchText"
md-items="item in fetchDevices(deviceSearchText, 10)"
md-item-text="item.name"
md-min-length="0"
placeholder="{{ 'device.device-list' | translate }}">
<md-item-template>
<span md-highlight-text="deviceSearchText" md-highlight-flags="^i">{{item.name}}</span>
</md-item-template>
<md-not-found>
<span translate translate-values='{ device: deviceSearchText }'>device.no-devices-matching</span>
</md-not-found>
</md-autocomplete>
<md-chip-template>
<span>
<strong>{{$chip.name}}</strong>
</span>
</md-chip-template>
</md-chips>
</section>
<section layout="row" flex ng-show="model.useFilter">
<md-input-container flex class="device-name-filter-input">
<label translate>device.name-starts-with</label>
<input ng-model="model.deviceNameFilter" aria-label="{{ 'device.name-starts-with' | translate }}">
</md-input-container>
</section>
<section class="tb-filter-switch" layout="column" layout-align="center center">
<label class="tb-small filter-label" translate>device.use-device-name-filter</label>
<md-switch class="filter-switch" ng-model="model.useFilter" aria-label="use-filter-switcher">
</md-switch>
</section>
</section>
<div class="tb-error-messages" ng-messages="ngModelCtrl.$error" role="alert">
<div translate ng-message="deviceList" class="tb-error-message">device.device-list-empty</div>
<div translate ng-message="deviceNameFilter" class="tb-error-message">device.device-name-filter-required</div>
<div translate translate-values='{ device: model.deviceNameFilter }' ng-message="deviceNameFilterDeviceMatch"
class="tb-error-message">device.device-name-filter-no-device-matched</div>
</div>
</section>

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

@ -197,7 +197,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
},
getLength: function () {
if (vm.items.hasNext) {
if (vm.items.hasNext && !vm.items.pending) {
return vm.items.rowData.length + pageSize;
} else {
return vm.items.rowData.length;
@ -206,7 +206,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
fetchMoreItems_: function () {
if (vm.items.hasNext && !vm.items.pending) {
var promise = vm.fetchItemsFunc(vm.items.nextPageLink);
var promise = vm.fetchItemsFunc(vm.items.nextPageLink, $scope.searchConfig.searchEntitySubtype);
if (promise) {
vm.items.pending = true;
promise.then(
@ -433,6 +433,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
reload();
});
$scope.$on('searchEntitySubtypeUpdated', function () {
reload();
});
vm.onGridInited(vm);
vm.itemRows.getItemAtIndex(pageSize);
@ -441,18 +445,16 @@ function GridController($scope, $state, $mdDialog, $document, $q, $timeout, $tra
if (vm.items && vm.items.pending) {
vm.items.reloadPending = true;
} else {
vm.items = {
data: [],
rowData: [],
nextPageLink: {
limit: pageSize,
textSearch: $scope.searchConfig.searchText
},
selections: {},
selectedCount: 0,
hasNext: true,
pending: false
vm.items.data.length = 0;
vm.items.rowData.length = 0;
vm.items.nextPageLink = {
limit: pageSize,
textSearch: $scope.searchConfig.searchText
};
vm.items.selections = {};
vm.items.selectedCount = 0;
vm.items.hasNext = true;
vm.items.pending = false;
vm.detailsConfig.isDetailsOpen = false;
vm.items.reloadPending = false;
vm.itemRows.getItemAtIndex(pageSize);

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

@ -24,9 +24,8 @@
<md-virtual-repeat-container ng-show="vm.hasData()" tb-scope-element="repeatContainer" id="tb-vertical-container" md-top-index="vm.topIndex" flex>
<div class="md-padding" layout="column">
<section layout="row" md-virtual-repeat="rowItem in vm.itemRows" md-on-demand md-item-size="vm.itemHeight">
<div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}">
<md-card ng-if="rowItem[n]"
ng-class="{'tb-current-item': vm.isCurrentItem(rowItem[n])}"
<div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}" ng-if="rowItem[n]">
<md-card ng-class="{'tb-current-item': vm.isCurrentItem(rowItem[n])}"
class="repeated-item tb-card-item" ng-style="{'height':(vm.itemHeight-16)+'px','cursor':'pointer'}"
ng-click="vm.clickItemFunc($event, rowItem[n])">
<section layout="row" layout-wrap>
@ -43,7 +42,7 @@
</md-card-title>
</section>
<md-card-content flex>
<tb-grid-card-content grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
<tb-grid-card-content flex grid-ctl="vm" parent-ctl="vm.parentCtl" item-controller="vm.itemCardController" item-template="vm.itemCardTemplate" item="rowItem[n]"></tb-grid-card-content>
</md-card-content>
<md-card-actions layout="row" layout-align="end end">
<md-button ng-if="action.isEnabled(rowItem[n])" ng-disabled="loading" class="md-icon-button md-primary" ng-repeat="action in vm.actionsList"
@ -56,6 +55,8 @@
</md-card-actions>
</md-card>
</div>
<div flex ng-repeat="n in [] | range:vm.columns" ng-style="{'height':vm.itemHeight+'px'}" ng-if="!rowItem[n]">
</div>
</section>
</div>
</md-virtual-repeat-container>

32
ui/src/app/dashboard/aliases-device-select-button.tpl.html

@ -1,32 +0,0 @@
<!--
Copyright © 2016-2017 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.
-->
<section class="tb-aliases-device-select" layout='row' layout-align="start center" ng-style="{minHeight: '32px', padding: '0 6px'}">
<md-button class="md-icon-button" aria-label="{{ 'dashboard.select-devices' | translate }}" ng-click="openEditMode($event)">
<md-tooltip md-direction="{{tooltipDirection}}">
{{ 'dashboard.select-devices' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'dashboard.select-devices' | translate }}" class="material-icons">devices_other</md-icon>
</md-button>
<span hide-xs hide-sm ng-click="openEditMode($event)">
<md-tooltip md-direction="{{tooltipDirection}}">
{{ 'dashboard.select-devices' | translate }}
</md-tooltip>
{{displayValue}}
</span>
</section>

31
ui/src/app/dashboard/aliases-device-select-panel.controller.js

@ -1,31 +0,0 @@
/*
* Copyright © 2016-2017 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.
*/
/*@ngInject*/
export default function AliasesDeviceSelectPanelController(mdPanelRef, $scope, types, deviceAliases, deviceAliasesInfo, onDeviceAliasesUpdate) {
var vm = this;
vm._mdPanelRef = mdPanelRef;
vm.deviceAliases = deviceAliases;
vm.deviceAliasesInfo = deviceAliasesInfo;
vm.onDeviceAliasesUpdate = onDeviceAliasesUpdate;
$scope.$watch('vm.deviceAliases', function () {
if (onDeviceAliasesUpdate) {
onDeviceAliasesUpdate(vm.deviceAliases);
}
}, true);
}

33
ui/src/app/dashboard/aliases-device-select-panel.tpl.html

@ -1,33 +0,0 @@
<!--
Copyright © 2016-2017 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-content flex layout="column">
<section flex layout="column">
<md-content flex class="md-padding" layout="column">
<div flex layout="row" ng-repeat="(aliasId, deviceAlias) in vm.deviceAliases">
<md-input-container flex>
<label>{{deviceAlias.alias}}</label>
<md-select ng-model="vm.deviceAliases[aliasId].deviceId">
<md-option ng-repeat="deviceInfo in vm.deviceAliasesInfo[aliasId]" ng-value="deviceInfo.id">
{{deviceInfo.name}}
</md-option>
</md-select>
</md-input-container>
</div>
</md-content>
</section>
</md-content>

153
ui/src/app/dashboard/aliases-device-select.directive.js

@ -1,153 +0,0 @@
/*
* Copyright © 2016-2017 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 './aliases-device-select.scss';
import $ from 'jquery';
/* eslint-disable import/no-unresolved, import/default */
import aliasesDeviceSelectButtonTemplate from './aliases-device-select-button.tpl.html';
import aliasesDeviceSelectPanelTemplate from './aliases-device-select-panel.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/* eslint-disable angular/angularelement */
/*@ngInject*/
export default function AliasesDeviceSelectDirective($compile, $templateCache, $mdMedia, types, $mdPanel, $document, $translate) {
var linker = function (scope, element, attrs, ngModelCtrl) {
/* tbAliasesDeviceSelect (ng-model)
* {
* "aliasId": {
* alias: alias,
* deviceId: deviceId
* }
* }
*/
var template = $templateCache.get(aliasesDeviceSelectButtonTemplate);
scope.tooltipDirection = angular.isDefined(attrs.tooltipDirection) ? attrs.tooltipDirection : 'top';
element.html(template);
scope.openEditMode = function (event) {
if (scope.disabled) {
return;
}
var position;
var panelHeight = $mdMedia('min-height: 350px') ? 250 : 150;
var panelWidth = 300;
var offset = element[0].getBoundingClientRect();
var bottomY = offset.bottom - $(window).scrollTop(); //eslint-disable-line
var leftX = offset.left - $(window).scrollLeft(); //eslint-disable-line
var yPosition;
var xPosition;
if (bottomY + panelHeight > $( window ).height()) { //eslint-disable-line
yPosition = $mdPanel.yPosition.ABOVE;
} else {
yPosition = $mdPanel.yPosition.BELOW;
}
if (leftX + panelWidth > $( window ).width()) { //eslint-disable-line
xPosition = $mdPanel.xPosition.CENTER;
} else {
xPosition = $mdPanel.xPosition.ALIGN_START;
}
position = $mdPanel.newPanelPosition()
.relativeTo(element)
.addPanelPosition(xPosition, yPosition);
var config = {
attachTo: angular.element($document[0].body),
controller: 'AliasesDeviceSelectPanelController',
controllerAs: 'vm',
templateUrl: aliasesDeviceSelectPanelTemplate,
panelClass: 'tb-aliases-device-select-panel',
position: position,
fullscreen: false,
locals: {
'deviceAliases': angular.copy(scope.model),
'deviceAliasesInfo': scope.deviceAliasesInfo,
'onDeviceAliasesUpdate': function (deviceAliases) {
scope.model = deviceAliases;
scope.updateView();
}
},
openFrom: event,
clickOutsideToClose: true,
escapeToClose: true,
focusOnOpen: false
};
$mdPanel.open(config);
}
scope.updateView = function () {
var value = angular.copy(scope.model);
ngModelCtrl.$setViewValue(value);
updateDisplayValue();
}
ngModelCtrl.$render = function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
scope.model = angular.copy(value);
updateDisplayValue();
}
}
function updateDisplayValue() {
var displayValue;
var singleValue = true;
var currentAliasId;
for (var aliasId in scope.model) {
if (!currentAliasId) {
currentAliasId = aliasId;
} else {
singleValue = false;
break;
}
}
if (singleValue && currentAliasId) {
var deviceId = scope.model[currentAliasId].deviceId;
var devicesInfo = scope.deviceAliasesInfo[currentAliasId];
for (var i=0;i<devicesInfo.length;i++) {
if (devicesInfo[i].id === deviceId) {
displayValue = devicesInfo[i].name;
break;
}
}
} else {
displayValue = $translate.instant('device.devices');
}
scope.displayValue = displayValue;
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
scope: {
deviceAliasesInfo:'='
},
link: linker
};
}
/* eslint-enable angular/angularelement */

46
ui/src/app/dashboard/aliases-device-select.scss

@ -1,46 +0,0 @@
/**
* Copyright © 2016-2017 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-panel {
&.tb-aliases-device-select-panel {
position: absolute;
}
}
.tb-aliases-device-select-panel {
max-height: 150px;
@media (min-height: 350px) {
max-height: 250px;
}
min-width: 300px;
background: white;
border-radius: 4px;
box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2),
0 13px 19px 2px rgba(0, 0, 0, 0.14),
0 5px 24px 4px rgba(0, 0, 0, 0.12);
overflow-x: hidden;
overflow-y: auto;
md-content {
background-color: #fff;
}
}
.tb-aliases-device-select {
span {
pointer-events: all;
cursor: pointer;
}
}

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

@ -1,227 +0,0 @@
/*
* Copyright © 2016-2017 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 './device-aliases.scss';
/*@ngInject*/
export default function DeviceAliasesController(deviceService, toast, $scope, $mdDialog, $document, $q, $translate,
types, config) {
var vm = this;
vm.isSingleDeviceAlias = config.isSingleDeviceAlias;
vm.singleDeviceAlias = config.singleDeviceAlias;
vm.deviceAliases = [];
vm.title = config.customTitle ? config.customTitle : 'device.aliases';
vm.disableAdd = config.disableAdd;
vm.aliasToWidgetsMap = {};
vm.onFilterDeviceChanged = onFilterDeviceChanged;
vm.addAlias = addAlias;
vm.removeAlias = removeAlias;
vm.cancel = cancel;
vm.save = save;
initController();
function initController() {
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);
}
}
}
}
}
}
if (vm.isSingleDeviceAlias) {
if (!vm.singleDeviceAlias.deviceFilter || vm.singleDeviceAlias.deviceFilter == null) {
vm.singleDeviceAlias.deviceFilter = {
useFilter: false,
deviceNameFilter: '',
deviceList: [],
};
}
}
for (aliasId in config.deviceAliases) {
var deviceAlias = config.deviceAliases[aliasId];
var alias = deviceAlias.alias;
var deviceFilter;
if (!deviceAlias.deviceFilter) {
deviceFilter = {
useFilter: false,
deviceNameFilter: '',
deviceList: [],
};
if (deviceAlias.deviceId) {
deviceFilter.deviceList = [deviceAlias.deviceId];
} else {
deviceFilter.deviceList = [];
}
} else {
deviceFilter = deviceAlias.deviceFilter;
}
var result = {id: aliasId, alias: alias, deviceFilter: deviceFilter, changed: true};
vm.deviceAliases.push(result);
}
}
function onFilterDeviceChanged(device, deviceAlias) {
if (deviceAlias) {
if (!deviceAlias.alias || deviceAlias.alias.length == 0) {
deviceAlias.changed = false;
}
if (!deviceAlias.changed && device) {
deviceAlias.alias = device.name;
}
}
}
function addAlias() {
var aliasId = 0;
for (var a in vm.deviceAliases) {
aliasId = Math.max(vm.deviceAliases[a].id, aliasId);
}
aliasId++;
var deviceAlias = {id: aliasId, alias: '', deviceFilter: {useFilter: false, deviceNameFilter: '', deviceList: []}, changed: false};
vm.deviceAliases.push(deviceAlias);
}
function removeAlias($event, deviceAlias) {
var index = vm.deviceAliases.indexOf(deviceAlias);
if (index > -1) {
var widgetsTitleList = vm.aliasToWidgetsMap[deviceAlias.id];
if (widgetsTitleList) {
var widgetsListHtml = '';
for (var t in widgetsTitleList) {
widgetsListHtml += '<br/>\'' + widgetsTitleList[t] + '\'';
}
var alert = $mdDialog.alert()
.parent(angular.element($document[0].body))
.clickOutsideToClose(true)
.title($translate.instant('device.unable-delete-device-alias-title'))
.htmlContent($translate.instant('device.unable-delete-device-alias-text', {deviceAlias: deviceAlias.alias, widgetsList: widgetsListHtml}))
.ariaLabel($translate.instant('device.unable-delete-device-alias-title'))
.ok($translate.instant('action.close'))
.targetEvent($event);
alert._options.skipHide = true;
alert._options.fullscreen = true;
$mdDialog.show(alert);
} else {
vm.deviceAliases.splice(index, 1);
if ($scope.theForm) {
$scope.theForm.$setDirty();
}
}
}
}
function cancel() {
$mdDialog.cancel();
}
function cleanupDeviceFilter(deviceFilter) {
if (deviceFilter.useFilter) {
deviceFilter.deviceList = [];
} else {
deviceFilter.deviceNameFilter = '';
}
return deviceFilter;
}
function save() {
var deviceAliases = {};
var uniqueAliasList = {};
var valid = true;
var aliasId, maxAliasId;
var alias;
var i;
if (vm.isSingleDeviceAlias) {
maxAliasId = 0;
vm.singleDeviceAlias.deviceFilter = cleanupDeviceFilter(vm.singleDeviceAlias.deviceFilter);
for (i in vm.deviceAliases) {
aliasId = vm.deviceAliases[i].id;
alias = vm.deviceAliases[i].alias;
if (alias === vm.singleDeviceAlias.alias) {
valid = false;
break;
}
maxAliasId = Math.max(aliasId, maxAliasId);
}
maxAliasId++;
vm.singleDeviceAlias.id = maxAliasId;
} else {
for (i in vm.deviceAliases) {
aliasId = vm.deviceAliases[i].id;
alias = vm.deviceAliases[i].alias;
if (!uniqueAliasList[alias]) {
uniqueAliasList[alias] = alias;
deviceAliases[aliasId] = {alias: alias, deviceFilter: cleanupDeviceFilter(vm.deviceAliases[i].deviceFilter)};
} else {
valid = false;
break;
}
}
}
if (valid) {
$scope.theForm.$setPristine();
if (vm.isSingleDeviceAlias) {
$mdDialog.hide(vm.singleDeviceAlias);
} else {
$mdDialog.hide(deviceAliases);
}
} else {
toast.showError($translate.instant('device.duplicate-alias-error', {alias: alias}));
}
}
}

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

@ -1,94 +0,0 @@
<!--
Copyright © 2016-2017 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 class="tb-aliases-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.isSingleDeviceAlias ? ('device.configure-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>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" 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 ng-show="vm.isSingleDeviceAlias">
<tb-device-filter ng-model="vm.singleDeviceAlias.deviceFilter">
</tb-device-filter>
</div>
<div ng-show="!vm.isSingleDeviceAlias" flex layout="row" layout-align="start center">
<span flex="5"></span>
<div flex layout="row" layout-align="start center"
style="padding: 0 0 0 10px; margin: 5px;">
<span translate flex="40" style="min-width: 100px;">device.alias</span>
<span translate flex="60" style="min-width: 190px; padding-left: 10px;">device.devices</span>
<span style="min-width: 40px;"></span>
</div>
</div>
<div ng-show="!vm.isSingleDeviceAlias" style="max-height: 500px; overflow: auto; padding-bottom: 20px;">
<div ng-form name="aliasForm" flex layout="row" layout-align="start center" ng-repeat="deviceAlias in vm.deviceAliases track by $index">
<span flex="5">{{$index + 1}}.</span>
<div class="md-whiteframe-4dp tb-alias" flex layout="row" layout-align="start center">
<md-input-container flex="40" style="min-width: 100px;" md-no-float class="md-block">
<input required ng-change="deviceAlias.changed=true" name="alias" placeholder="{{ 'device.alias' | translate }}" ng-model="deviceAlias.alias">
<div ng-messages="aliasForm.alias.$error">
<div translate ng-message="required">device.alias-required</div>
</div>
</md-input-container>
<section flex="60" layout="column">
<tb-device-filter style="padding-left: 10px;"
ng-model="deviceAlias.deviceFilter"
on-matching-device-change="vm.onFilterDeviceChanged(device, deviceAlias)">
</tb-device-filter>
</section>
<md-button ng-disabled="loading" class="md-icon-button md-primary" style="min-width: 40px;"
ng-click="vm.removeAlias($event, deviceAlias)" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'device.remove-alias' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">
close
</md-icon>
</md-button>
</div>
</div>
</div>
<div ng-show="!vm.isSingleDeviceAlias && !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 }}
</md-tooltip>
<span translate>action.add</span>
</md-button>
</div>
</fieldset>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit" class="md-raised md-primary">
{{ 'action.save' | 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>

2
ui/src/app/dashboard/states/manage-dashboard-states.tpl.html

@ -58,7 +58,7 @@
{{ 'dashboard.search-states' | translate }}
</md-tooltip>
</md-button>
<md-input-container md-theme="tb-search-input" flex>
<md-input-container flex>
<label>&nbsp;</label>
<input ng-model="vm.query.search" placeholder="{{ 'dashboard.search-states' | translate }}"/>
</md-input-container>

50
ui/src/app/device/attribute/add-attribute-dialog.controller.js

@ -1,50 +0,0 @@
/*
* Copyright © 2016-2017 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.
*/
/*@ngInject*/
export default function AddAttributeDialogController($scope, $mdDialog, types, deviceService, deviceId, attributeScope) {
var vm = this;
vm.attribute = {};
vm.valueTypes = types.valueType;
vm.valueType = types.valueType.string;
vm.add = add;
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
function add() {
$scope.theForm.$setPristine();
deviceService.saveDeviceAttributes(deviceId, attributeScope, [vm.attribute]).then(
function success() {
$mdDialog.hide();
}
);
}
$scope.$watch('vm.valueType', function() {
if (vm.valueType === types.valueType.boolean) {
vm.attribute.value = false;
} else {
vm.attribute.value = null;
}
});
}

95
ui/src/app/device/attribute/add-attribute-dialog.tpl.html

@ -1,95 +0,0 @@
<!--
Copyright © 2016-2017 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="{{ 'attribute.add' | translate }}" style="min-width: 400px;">
<form name="theForm" ng-submit="vm.add()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>attribute.add</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-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<md-content class="md-padding" layout="column">
<fieldset ng-disabled="loading">
<md-input-container class="md-block">
<label translate>attribute.key</label>
<input required name="key" ng-model="vm.attribute.key">
<div ng-messages="theForm.key.$error">
<div translate ng-message="required">attribute.key-required</div>
</div>
</md-input-container>
<section layout="row">
<md-input-container flex="40" class="md-block" style="width: 200px;">
<label translate>value.type</label>
<md-select ng-model="vm.valueType" ng-disabled="loading()">
<md-option ng-repeat="type in vm.valueTypes" ng-value="type">
<md-icon md-svg-icon="{{ type.icon }}"></md-icon>
<span>{{type.name | translate}}</span>
</md-option>
</md-select>
</md-input-container>
<md-input-container ng-if="vm.valueType===vm.valueTypes.string" flex="60" class="md-block">
<label translate>value.string-value</label>
<input required name="value" ng-model="vm.attribute.value">
<div ng-messages="theForm.value.$error">
<div translate ng-message="required">attribute.value-required</div>
</div>
</md-input-container>
<md-input-container ng-if="vm.valueType===vm.valueTypes.integer" flex="60" class="md-block">
<label translate>value.integer-value</label>
<input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="vm.attribute.value">
<div ng-messages="theForm.value.$error">
<div translate ng-message="required">attribute.value-required</div>
<div translate ng-message="pattern">value.invalid-integer-value</div>
</div>
</md-input-container>
<md-input-container ng-if="vm.valueType===vm.valueTypes.double" flex="60" class="md-block">
<label translate>value.double-value</label>
<input required name="value" type="number" step="any" ng-model="vm.attribute.value">
<div ng-messages="theForm.value.$error">
<div translate ng-message="required">attribute.value-required</div>
</div>
</md-input-container>
<div layout="column" layout-align="center" flex="60" ng-if="vm.valueType===vm.valueTypes.boolean">
<md-checkbox ng-model="vm.attribute.value" style="margin-bottom: 0px;">
{{ (vm.attribute.value ? 'value.true' : 'value.false') | translate }}
</md-checkbox>
</div>
</section>
</fieldset>
</md-content>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary">
{{ 'action.add' | 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>

73
ui/src/app/device/attribute/add-widget-to-dashboard-dialog.controller.js

@ -1,73 +0,0 @@
/*
* Copyright © 2016-2017 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.
*/
/*@ngInject*/
export default function AddWidgetToDashboardDialogController($scope, $mdDialog, $state, itembuffer, dashboardService, deviceId, deviceName, widget) {
var vm = this;
vm.widget = widget;
vm.dashboardId = null;
vm.addToDashboardType = 0;
vm.newDashboard = {};
vm.openDashboard = false;
vm.add = add;
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
function add() {
$scope.theForm.$setPristine();
if (vm.addToDashboardType === 0) {
dashboardService.getDashboard(vm.dashboardId).then(
function success(dashboard) {
addWidgetToDashboard(dashboard);
},
function fail() {}
);
} else {
addWidgetToDashboard(vm.newDashboard);
}
}
function addWidgetToDashboard(theDashboard) {
var aliasesInfo = {
datasourceAliases: {},
targetDeviceAliases: {}
};
aliasesInfo.datasourceAliases[0] = {
aliasName: deviceName,
deviceFilter: {
useFilter: false,
deviceNameFilter: '',
deviceList: [deviceId]
}
};
theDashboard = itembuffer.addWidgetToDashboard(theDashboard, vm.widget, aliasesInfo, null, 48, -1, -1);
dashboardService.saveDashboard(theDashboard).then(
function success(dashboard) {
$mdDialog.hide();
if (vm.openDashboard) {
$state.go('home.dashboards.dashboard', {dashboardId: dashboard.id.id});
}
}
);
}
}

81
ui/src/app/device/attribute/add-widget-to-dashboard-dialog.tpl.html

@ -1,81 +0,0 @@
<!--
Copyright © 2016-2017 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="{{ 'attribute.add-widget-to-dashboard' | translate }}" style="min-width: 400px;">
<form name="theForm" ng-submit="vm.add()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>attribute.add-widget-to-dashboard</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-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<md-content class="md-padding" layout="column">
<fieldset ng-disabled="loading">
<md-radio-group ng-model="vm.addToDashboardType" class="md-primary">
<md-radio-button flex ng-value=0 class="md-primary md-align-top-left md-radio-interactive">
<section flex layout="column" style="width: 300px;">
<span translate style="padding-bottom: 10px;">dashboard.select-existing</span>
<tb-dashboard-autocomplete the-form="theForm"
ng-disabled="loading || vm.addToDashboardType != 0"
tb-required="vm.addToDashboardType === 0"
ng-model="vm.dashboardId"
select-first-dashboard="false">
</tb-dashboard-autocomplete>
</section>
</md-radio-button>
<md-radio-button flex ng-value=1 class="md-primary md-align-top-left md-radio-interactive">
<section flex layout="column" style="width: 300px;">
<span translate>dashboard.create-new</span>
<md-input-container class="md-block">
<label translate>dashboard.new-dashboard-title</label>
<input ng-required="vm.addToDashboardType === 1" name="title" ng-model="vm.newDashboard.title">
<div ng-messages="theForm.title.$error">
<div translate ng-message="required">dashboard.title-required</div>
</div>
</md-input-container>
</section>
</md-radio-button>
</md-radio-group>
</fieldset>
</md-content>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-checkbox
ng-model="vm.openDashboard"
aria-label="{{ 'dashboard.open-dashboard' | translate }}"
style="margin-bottom: 0px; padding-right: 20px;">
{{ 'dashboard.open-dashboard' | translate }}
</md-checkbox>
<md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary">
{{ 'action.add' | 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>

421
ui/src/app/device/attribute/attribute-table.directive.js

@ -1,421 +0,0 @@
/*
* Copyright © 2016-2017 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 'angular-material-data-table/dist/md-data-table.min.css';
import './attribute-table.scss';
/* eslint-disable import/no-unresolved, import/default */
import attributeTableTemplate from './attribute-table.tpl.html';
import addAttributeDialogTemplate from './add-attribute-dialog.tpl.html';
import addWidgetToDashboardDialogTemplate from './add-widget-to-dashboard-dialog.tpl.html';
import editAttributeValueTemplate from './edit-attribute-value.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import EditAttributeValueController from './edit-attribute-value.controller';
/*@ngInject*/
export default function AttributeTableDirective($compile, $templateCache, $rootScope, $q, $mdEditDialog, $mdDialog,
$document, $translate, $filter, utils, types, dashboardService, deviceService, widgetService) {
var linker = function (scope, element, attrs) {
var template = $templateCache.get(attributeTableTemplate);
element.html(template);
scope.types = types;
scope.attributeScopes = types.deviceAttributesScope;
var getAttributeScopeByValue = function(attributeScopeValue) {
if (scope.types.latestTelemetry.value === attributeScopeValue) {
return scope.types.latestTelemetry;
}
for (var attrScope in scope.attributeScopes) {
if (scope.attributeScopes[attrScope].value === attributeScopeValue) {
return scope.attributeScopes[attrScope];
}
}
}
scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
scope.attributes = {
count: 0,
data: []
};
scope.selectedAttributes = [];
scope.mode = 'default'; // 'widget'
scope.subscriptionId = null;
scope.query = {
order: 'key',
limit: 5,
page: 1,
search: null
};
scope.$watch("deviceId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
scope.resetFilter();
scope.getDeviceAttributes(false, true);
}
});
scope.$watch("attributeScope", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
scope.mode = 'default';
scope.query.search = null;
scope.selectedAttributes = [];
scope.getDeviceAttributes(false, true);
}
});
scope.resetFilter = function() {
scope.mode = 'default';
scope.query.search = null;
scope.selectedAttributes = [];
scope.attributeScope = getAttributeScopeByValue(attrs.defaultAttributeScope);
}
scope.enterFilterMode = function() {
scope.query.search = '';
}
scope.exitFilterMode = function() {
scope.query.search = null;
scope.getDeviceAttributes();
}
scope.$watch("query.search", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && scope.query.search != null) {
scope.getDeviceAttributes();
}
});
function success(attributes, update, apply) {
scope.attributes = attributes;
if (!update) {
scope.selectedAttributes = [];
}
if (apply) {
scope.$digest();
}
}
scope.onReorder = function() {
scope.getDeviceAttributes(false, false);
}
scope.onPaginate = function() {
scope.getDeviceAttributes(false, false);
}
scope.getDeviceAttributes = function(forceUpdate, reset) {
if (scope.attributesDeferred) {
scope.attributesDeferred.resolve();
}
if (scope.deviceId && scope.attributeScope) {
if (reset) {
scope.attributes = {
count: 0,
data: []
};
}
scope.checkSubscription();
scope.attributesDeferred = deviceService.getDeviceAttributes(scope.deviceId, scope.attributeScope.value,
scope.query, function(attributes, update, apply) {
success(attributes, update || forceUpdate, apply);
}
);
} else {
var deferred = $q.defer();
scope.attributesDeferred = deferred;
success({
count: 0,
data: []
});
deferred.resolve();
}
}
scope.checkSubscription = function() {
var newSubscriptionId = null;
if (scope.deviceId && scope.attributeScope.clientSide && scope.mode != 'widget') {
newSubscriptionId = deviceService.subscribeForDeviceAttributes(scope.deviceId, scope.attributeScope.value);
}
if (scope.subscriptionId && scope.subscriptionId != newSubscriptionId) {
deviceService.unsubscribeForDeviceAttributes(scope.subscriptionId);
}
scope.subscriptionId = newSubscriptionId;
}
scope.$on('$destroy', function() {
if (scope.subscriptionId) {
deviceService.unsubscribeForDeviceAttributes(scope.subscriptionId);
}
});
scope.editAttribute = function($event, attribute) {
if (!scope.attributeScope.clientSide) {
$event.stopPropagation();
$mdEditDialog.show({
controller: EditAttributeValueController,
templateUrl: editAttributeValueTemplate,
locals: {attributeValue: attribute.value,
save: function (model) {
var updatedAttribute = angular.copy(attribute);
updatedAttribute.value = model.value;
deviceService.saveDeviceAttributes(scope.deviceId, scope.attributeScope.value, [updatedAttribute]).then(
function success() {
scope.getDeviceAttributes();
}
);
}},
targetEvent: $event
});
}
}
scope.addAttribute = function($event) {
if (!scope.attributeScope.clientSide) {
$event.stopPropagation();
$mdDialog.show({
controller: 'AddAttributeDialogController',
controllerAs: 'vm',
templateUrl: addAttributeDialogTemplate,
parent: angular.element($document[0].body),
locals: {deviceId: scope.deviceId, attributeScope: scope.attributeScope.value},
fullscreen: true,
targetEvent: $event
}).then(function () {
scope.getDeviceAttributes();
});
}
}
scope.deleteAttributes = function($event) {
if (!scope.attributeScope.clientSide) {
$event.stopPropagation();
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title($translate.instant('attribute.delete-attributes-title', {count: scope.selectedAttributes.length}, 'messageformat'))
.htmlContent($translate.instant('attribute.delete-attributes-text'))
.ariaLabel($translate.instant('attribute.delete-attributes'))
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
deviceService.deleteDeviceAttributes(scope.deviceId, scope.attributeScope.value, scope.selectedAttributes).then(
function success() {
scope.selectedAttributes = [];
scope.getDeviceAttributes();
}
)
});
}
}
scope.nextWidget = function() {
if (scope.widgetsCarousel.index < scope.widgetsList.length-1) {
scope.widgetsCarousel.index++;
}
}
scope.prevWidget = function() {
if (scope.widgetsCarousel.index > 0) {
scope.widgetsCarousel.index--;
}
}
scope.enterWidgetMode = function() {
if (scope.widgetsIndexWatch) {
scope.widgetsIndexWatch();
scope.widgetsIndexWatch = null;
}
if (scope.widgetsBundleWatch) {
scope.widgetsBundleWatch();
scope.widgetsBundleWatch = null;
}
scope.mode = 'widget';
scope.checkSubscription();
scope.widgetsList = [];
scope.widgetsListCache = [];
scope.widgetsLoaded = false;
scope.widgetsCarousel = {
index: 0
}
scope.widgetsBundle = null;
scope.firstBundle = true;
scope.selectedWidgetsBundleAlias = types.systemBundleAlias.cards;
scope.aliasesInfo = {
deviceAliases: {
'1': {alias: scope.deviceName, deviceId: scope.deviceId}
},
deviceAliasesInfo: {
'1': [
{name: scope.deviceName, id: scope.deviceId}
]
}
};
var dataKeyType = scope.attributeScope === types.latestTelemetry ?
types.dataKeyType.timeseries : types.dataKeyType.attribute;
var datasource = {
type: types.datasourceType.device,
deviceAliasId: '1',
dataKeys: []
}
var i = 0;
for (var attr =0; attr < scope.selectedAttributes.length;attr++) {
var attribute = scope.selectedAttributes[attr];
var dataKey = {
name: attribute.key,
label: attribute.key,
type: dataKeyType,
color: utils.getMaterialColor(i),
settings: {},
_hash: Math.random()
}
datasource.dataKeys.push(dataKey);
i++;
}
scope.widgetsIndexWatch = scope.$watch('widgetsCarousel.index', function(newVal, prevVal) {
if (scope.mode === 'widget' && (newVal != prevVal)) {
var index = scope.widgetsCarousel.index;
for (var i = 0; i < scope.widgetsList.length; i++) {
scope.widgetsList[i].splice(0, scope.widgetsList[i].length);
if (i === index) {
scope.widgetsList[i].push(scope.widgetsListCache[i][0]);
}
}
}
});
scope.widgetsBundleWatch = scope.$watch('widgetsBundle', function(newVal, prevVal) {
if (scope.mode === 'widget' && (scope.firstBundle === true || newVal != prevVal)) {
scope.widgetsList = [];
scope.widgetsListCache = [];
scope.widgetsCarousel.index = 0;
scope.firstBundle = false;
if (scope.widgetsBundle) {
scope.widgetsLoaded = false;
var bundleAlias = scope.widgetsBundle.alias;
var isSystem = scope.widgetsBundle.tenantId.id === types.id.nullUid;
widgetService.getBundleWidgetTypes(scope.widgetsBundle.alias, isSystem).then(
function success(widgetTypes) {
widgetTypes = $filter('orderBy')(widgetTypes, ['-descriptor.type','-createdTime']);
for (var i = 0; i < widgetTypes.length; i++) {
var widgetType = widgetTypes[i];
var widgetInfo = widgetService.toWidgetInfo(widgetType);
if (widgetInfo.type !== types.widgetType.static.value) {
var sizeX = widgetInfo.sizeX * 2;
var sizeY = widgetInfo.sizeY * 2;
var col = Math.floor(Math.max(0, (20 - sizeX) / 2));
var widget = {
isSystemType: isSystem,
bundleAlias: bundleAlias,
typeAlias: widgetInfo.alias,
type: widgetInfo.type,
title: widgetInfo.widgetName,
sizeX: sizeX,
sizeY: sizeY,
row: 0,
col: col,
config: angular.fromJson(widgetInfo.defaultConfig)
};
widget.config.title = widgetInfo.widgetName;
widget.config.datasources = [datasource];
var length;
if (scope.attributeScope === types.latestTelemetry && widgetInfo.type !== types.widgetType.rpc.value) {
length = scope.widgetsListCache.push([widget]);
scope.widgetsList.push(length === 1 ? [widget] : []);
} else if (widgetInfo.type === types.widgetType.latest.value) {
length = scope.widgetsListCache.push([widget]);
scope.widgetsList.push(length === 1 ? [widget] : []);
}
}
}
scope.widgetsLoaded = true;
}
);
}
}
});
}
scope.exitWidgetMode = function() {
if (scope.widgetsBundleWatch) {
scope.widgetsBundleWatch();
scope.widgetsBundleWatch = null;
}
if (scope.widgetsIndexWatch) {
scope.widgetsIndexWatch();
scope.widgetsIndexWatch = null;
}
scope.selectedWidgetsBundleAlias = null;
scope.mode = 'default';
scope.getDeviceAttributes(true);
}
scope.getServerTimeDiff = function() {
return dashboardService.getServerTimeDiff();
}
scope.addWidgetToDashboard = function($event) {
if (scope.mode === 'widget' && scope.widgetsListCache.length > 0) {
var widget = scope.widgetsListCache[scope.widgetsCarousel.index][0];
$event.stopPropagation();
$mdDialog.show({
controller: 'AddWidgetToDashboardDialogController',
controllerAs: 'vm',
templateUrl: addWidgetToDashboardDialogTemplate,
parent: angular.element($document[0].body),
locals: {deviceId: scope.deviceId, deviceName: scope.deviceName, widget: angular.copy(widget)},
fullscreen: true,
targetEvent: $event
}).then(function () {
});
}
}
scope.loading = function() {
return $rootScope.loading;
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
link: linker,
scope: {
deviceId: '=',
deviceName: '=',
disableAttributeScopeSelection: '@?'
}
};
}

50
ui/src/app/device/attribute/attribute-table.scss

@ -1,50 +0,0 @@
/**
* Copyright © 2016-2017 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 '../../../scss/constants';
$md-light: rgba(255, 255, 255, 100%);
$md-edit-icon-fill: #757575;
md-toolbar.md-table-toolbar.alternate {
.md-toolbar-tools {
md-icon {
color: $md-light;
}
}
}
.md-table {
.md-cell {
ng-md-icon {
fill: $md-edit-icon-fill;
float: right;
height: 16px;
}
}
}
.widgets-carousel {
position: relative;
margin: 0px;
min-height: 150px !important;
tb-dashboard {
#gridster-parent {
padding: 0 7px;
}
}
}

210
ui/src/app/device/attribute/attribute-table.tpl.html

@ -1,210 +0,0 @@
<!--
Copyright © 2016-2017 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-content flex class="md-padding tb-absolute-fill" layout="column">
<section layout="row" ng-show="!disableAttributeScopeSelection">
<md-input-container class="md-block" style="width: 200px;">
<label translate>attribute.attributes-scope</label>
<md-select ng-model="attributeScope" ng-disabled="loading()">
<md-option ng-repeat="scope in attributeScopes" ng-value="scope">
{{scope.name | translate}}
</md-option>
</md-select>
</md-input-container>
</section>
<div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
<md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
&& !selectedAttributes.length
&& query.search === null">
<div class="md-toolbar-tools">
<span translate>{{ attributeScope.name }}</span>
<span flex></span>
<md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="addAttribute($event)">
<md-icon>add</md-icon>
<md-tooltip md-direction="top">
{{ 'action.add' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="enterFilterMode()">
<md-icon>search</md-icon>
<md-tooltip md-direction="top">
{{ 'action.search' | translate }}
</md-tooltip>
</md-button>
<md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="getDeviceAttributes()">
<md-icon>refresh</md-icon>
<md-tooltip md-direction="top">
{{ 'action.refresh' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
&& !selectedAttributes.length
&& query.search != null">
<div class="md-toolbar-tools">
<md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
<md-tooltip md-direction="top">
{{ 'action.search' | translate }}
</md-tooltip>
</md-button>
<md-input-container md-theme="tb-search-input" flex>
<label>&nbsp;</label>
<input ng-model="query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
</md-input-container>
<md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="exitFilterMode()">
<md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
<md-tooltip md-direction="top">
{{ 'action.close' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<md-toolbar class="md-table-toolbar alternate" ng-show="mode==='default' && selectedAttributes.length">
<div class="md-toolbar-tools">
<span translate="{{attributeScope === types.latestTelemetry
? 'attribute.selected-telemetry'
: 'attribute.selected-attributes'}}"
translate-values="{count: selectedAttributes.length}"
translate-interpolation="messageformat"></span>
<span flex></span>
<md-button ng-show="!attributeScope.clientSide" class="md-icon-button" ng-click="deleteAttributes($event)">
<md-icon>delete</md-icon>
<md-tooltip md-direction="top">
{{ 'action.delete' | translate }}
</md-tooltip>
</md-button>
<md-button ng-show="attributeScope.clientSide" class="md-accent md-hue-2 md-raised" ng-click="enterWidgetMode()">
<md-tooltip md-direction="top">
{{ 'attribute.show-on-widget' | translate }}
</md-tooltip>
<md-icon>now_widgets</md-icon>
<span translate>attribute.show-on-widget</span>
</md-button>
</div>
</md-toolbar>
<md-toolbar class="md-table-toolbar alternate" ng-show="mode==='widget'">
<div class="md-toolbar-tools">
<div flex layout="row" layout-align="start">
<span class="tb-details-subtitle">{{ 'widgets-bundle.current' | translate }}</span>
<tb-widgets-bundle-select flex-offset="5"
flex
ng-model="widgetsBundle"
select-first-bundle="false"
select-bundle-alias="selectedWidgetsBundleAlias">
</tb-widgets-bundle-select>
</div>
<md-button ng-show="widgetsList.length > 0" class="md-accent md-hue-2 md-raised" ng-click="addWidgetToDashboard($event)">
<md-tooltip md-direction="top">
{{ 'attribute.add-to-dashboard' | translate }}
</md-tooltip>
<md-icon>dashboard</md-icon>
<span translate>attribute.add-to-dashboard</span>
</md-button>
<md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="exitWidgetMode()">
<md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
<md-tooltip md-direction="top">
{{ 'action.close' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<md-table-container ng-show="mode!='widget'">
<table md-table md-row-select multiple="" ng-model="selectedAttributes" md-progress="attributesDeferred.promise">
<thead md-head md-order="query.order" md-on-reorder="onReorder">
<tr md-row>
<th md-column md-order-by="lastUpdateTs"><span>Last update time</span></th>
<th md-column md-order-by="key"><span>Key</span></th>
<th md-column>Value</th>
</tr>
</thead>
<tbody md-body>
<tr md-row md-select="attribute" md-select-id="key" md-auto-select ng-repeat="attribute in attributes.data">
<td md-cell>{{attribute.lastUpdateTs | date : 'yyyy-MM-dd HH:mm:ss'}}</td>
<td md-cell>{{attribute.key}}</td>
<td md-cell ng-click="editAttribute($event, attribute)">
<span>{{attribute.value}}</span>
<span ng-show="!attributeScope.clientSide"><ng-md-icon size="16" icon="edit"></ng-md-icon></span>
</td>
</tr>
</tbody>
</table>
</md-table-container>
<md-table-pagination ng-show="mode!='widget'" md-limit="query.limit" md-limit-options="[5, 10, 15]"
md-page="query.page" md-total="{{attributes.count}}"
md-on-paginate="onPaginate" md-page-select>
</md-table-pagination>
<ul flex rn-carousel ng-if="mode==='widget'" class="widgets-carousel"
rn-carousel-index="widgetsCarousel.index"
rn-carousel-buffered
rn-carousel-transition="fadeAndSlide"
rn-swipe-disabled="true">
<li ng-repeat="widgets in widgetsList">
<tb-dashboard
aliases-info="aliasesInfo"
widgets="widgets"
get-st-diff="getServerTimeDiff()"
columns="20"
is-edit="false"
is-mobile-disabled="true"
is-edit-action-enabled="false"
is-remove-action-enabled="false">
</tb-dashboard>
</li>
<span translate ng-if="widgetsLoaded &&
widgetsList.length === 0 &&
widgetsBundle"
layout-align="center center"
style="text-transform: uppercase; display: flex;"
class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
<span translate ng-if="!widgetsBundle"
layout-align="center center"
style="text-transform: uppercase; display: flex;"
class="md-headline tb-absolute-fill">widget.select-widgets-bundle</span>
<div ng-show="widgetsList.length > 1"
style="position: absolute; left: 0; height: 100%;" layout="column" layout-align="center">
<md-button ng-show="widgetsCarousel.index > 0"
class="md-icon-button"
ng-click="prevWidget()">
<md-icon>navigate_before</md-icon>
<md-tooltip md-direction="top">
{{ 'attribute.prev-widget' | translate }}
</md-tooltip>
</md-button>
</div>
<div ng-show="widgetsList.length > 1"
style="position: absolute; right: 0; height: 100%;" layout="column" layout-align="center">
<md-button ng-show="widgetsCarousel.index < widgetsList.length-1"
class="md-icon-button"
ng-click="nextWidget()">
<md-icon>navigate_next</md-icon>
<md-tooltip md-direction="top">
{{ 'attribute.next-widget' | translate }}
</md-tooltip>
</md-button>
</div>
<div style="position: absolute; bottom: 0; width: 100%; font-size: 24px;" layout="row" layout-align="center">
<div rn-carousel-indicators
ng-if="widgetsList.length > 1"
slides="widgetsList"
rn-carousel-index="widgetsCarousel.index">
</div>
</div>
</ul>
</div>
</md-content>

71
ui/src/app/device/attribute/edit-attribute-value.controller.js

@ -1,71 +0,0 @@
/*
* Copyright © 2016-2017 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.
*/
/*@ngInject*/
export default function EditAttributeValueController($scope, $q, $element, types, attributeValue, save) {
$scope.valueTypes = types.valueType;
$scope.model = {};
$scope.model.value = attributeValue;
if ($scope.model.value === true || $scope.model.value === false) {
$scope.valueType = types.valueType.boolean;
} else if (angular.isNumber($scope.model.value)) {
if ($scope.model.value.toString().indexOf('.') == -1) {
$scope.valueType = types.valueType.integer;
} else {
$scope.valueType = types.valueType.double;
}
} else {
$scope.valueType = types.valueType.string;
}
$scope.submit = submit;
$scope.dismiss = dismiss;
function dismiss() {
$element.remove();
}
function update() {
if($scope.editDialog.$invalid) {
return $q.reject();
}
if(angular.isFunction(save)) {
return $q.when(save($scope.model));
}
return $q.resolve();
}
function submit() {
update().then(function () {
$scope.dismiss();
});
}
$scope.$watch('valueType', function(newVal, prevVal) {
if (newVal != prevVal) {
if ($scope.valueType === types.valueType.boolean) {
$scope.model.value = false;
} else {
$scope.model.value = null;
}
}
});
}

72
ui/src/app/device/attribute/edit-attribute-value.tpl.html

@ -1,72 +0,0 @@
<!--
Copyright © 2016-2017 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-edit-dialog>
<form name="editDialog" ng-submit="submit()">
<div layout="column" class="md-content" style="width: 400px;">
<fieldset>
<section layout="row">
<md-input-container flex="40" class="md-block">
<label translate>value.type</label>
<md-select ng-model="valueType">
<md-option ng-repeat="type in valueTypes" ng-value="type">
<md-icon md-svg-icon="{{ type.icon }}"></md-icon>
<span>{{type.name | translate}}</span>
</md-option>
</md-select>
</md-input-container>
<md-input-container ng-if="valueType===valueTypes.string" flex="60" class="md-block">
<label translate>value.string-value</label>
<input required name="value" ng-model="model.value">
<div ng-messages="editDialog.value.$error">
<div translate ng-message="required">attribute.value-required</div>
</div>
</md-input-container>
<md-input-container ng-if="valueType===valueTypes.integer" flex="60" class="md-block">
<label translate>value.integer-value</label>
<input required name="value" type="number" step="1" ng-pattern="/^-?[0-9]+$/" ng-model="model.value">
<div ng-messages="editDialog.value.$error">
<div translate ng-message="required">attribute.value-required</div>
<div translate ng-message="pattern">value.invalid-integer-value</div>
</div>
</md-input-container>
<md-input-container ng-if="valueType===valueTypes.double" flex="60" class="md-block">
<label translate>value.double-value</label>
<input required name="value" type="number" step="any" ng-model="model.value">
<div ng-messages="editDialog.value.$error">
<div translate ng-message="required">attribute.value-required</div>
</div>
</md-input-container>
<div layout="column" layout-align="center" flex="60" ng-if="valueType===valueTypes.boolean">
<md-checkbox ng-model="model.value" style="margin-bottom: 0px;">
{{ (model.value ? 'value.true' : 'value.false') | translate }}
</md-checkbox>
</div>
</section>
</fieldset>
</div>
<div layout="row" layout-align="end" class="md-actions">
<md-button ng-click="dismiss()">{{ 'action.cancel' |
translate }}
</md-button>
<md-button ng-disabled="editDialog.$invalid || !editDialog.$dirty" type="submit"
class="md-raised md-primary">
{{ 'action.update' | translate }}
</md-button>
</div>
</form>
</md-edit-dialog>

7
ui/src/app/device/device-card.tpl.html

@ -15,5 +15,8 @@
limitations under the License.
-->
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
<div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
<div flex layout="column" style="margin-top: -10px;">
<div flex style="text-transform: uppercase; padding-bottom: 10px;">{{vm.item.type}}</div>
<div class="tb-small" ng-show="vm.isAssignedToCustomer()">{{'device.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'</div>
<div class="tb-small" ng-show="vm.isPublic()">{{'device.public' | translate}}</div>
</div>

7
ui/src/app/device/device-fieldset.tpl.html

@ -66,6 +66,13 @@
<div translate ng-message="required">device.name-required</div>
</div>
</md-input-container>
<tb-entity-subtype-autocomplete
ng-disabled="loading || !isEdit"
tb-required="true"
the-form="theForm"
ng-model="device.type"
entity-type="types.entityType.device">
</tb-entity-subtype-autocomplete>
<md-input-container class="md-block">
<md-checkbox ng-disabled="loading || !isEdit" flex aria-label="{{ 'device.is-gateway' | translate }}"
ng-model="device.additionalInfo.gateway">{{ 'device.is-gateway' | translate }}

12
ui/src/app/device/device.controller.js

@ -48,7 +48,8 @@ export function DeviceCardController(types) {
/*@ngInject*/
export function DeviceController(userService, deviceService, customerService, $state, $stateParams, $document, $mdDialog, $q, $translate, types) {
export function DeviceController($rootScope, userService, deviceService, customerService, $state, $stateParams,
$document, $mdDialog, $q, $translate, types) {
var customerId = $stateParams.customerId;
@ -131,8 +132,8 @@ export function DeviceController(userService, deviceService, customerService, $s
}
if (vm.devicesScope === 'tenant') {
fetchDevicesFunction = function (pageLink) {
return deviceService.getTenantDevices(pageLink, true);
fetchDevicesFunction = function (pageLink, deviceType) {
return deviceService.getTenantDevices(pageLink, true, null, deviceType);
};
deleteDeviceFunction = function (deviceId) {
return deviceService.deleteDevice(deviceId);
@ -242,8 +243,8 @@ export function DeviceController(userService, deviceService, customerService, $s
} else if (vm.devicesScope === 'customer' || vm.devicesScope === 'customer_user') {
fetchDevicesFunction = function (pageLink) {
return deviceService.getCustomerDevices(customerId, pageLink, true);
fetchDevicesFunction = function (pageLink, deviceType) {
return deviceService.getCustomerDevices(customerId, pageLink, true, null, deviceType);
};
deleteDeviceFunction = function (deviceId) {
return deviceService.unassignDeviceFromCustomer(deviceId);
@ -368,6 +369,7 @@ export function DeviceController(userService, deviceService, customerService, $s
var deferred = $q.defer();
deviceService.saveDevice(device).then(
function success(savedDevice) {
$rootScope.$broadcast('deviceSaved');
var devices = [ savedDevice ];
customerService.applyAssignedCustomersInfo(devices).then(
function success(items) {

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

@ -25,6 +25,7 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl
var template = $templateCache.get(deviceFieldsetTemplate);
element.html(template);
scope.types = types;
scope.isAssignedToCustomer = false;
scope.isPublic = false;
scope.assignedCustomer = null;

6
ui/src/app/device/device.routes.js

@ -20,7 +20,7 @@ import devicesTemplate from './devices.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function DeviceRoutes($stateProvider) {
export default function DeviceRoutes($stateProvider, types) {
$stateProvider
.state('home.devices', {
url: '/devices',
@ -37,6 +37,8 @@ export default function DeviceRoutes($stateProvider) {
data: {
devicesType: 'tenant',
searchEnabled: true,
searchByEntitySubtype: true,
searchEntityType: types.entityType.device,
pageTitle: 'device.devices'
},
ncyBreadcrumb: {
@ -58,6 +60,8 @@ export default function DeviceRoutes($stateProvider) {
data: {
devicesType: 'customer',
searchEnabled: true,
searchByEntitySubtype: true,
searchEntityType: types.entityType.device,
pageTitle: 'customer.devices'
},
ncyBreadcrumb: {

2
ui/src/app/entity/attribute/attribute-table.tpl.html

@ -63,7 +63,7 @@
{{ 'action.search' | translate }}
</md-tooltip>
</md-button>
<md-input-container md-theme="tb-search-input" flex>
<md-input-container flex>
<label>&nbsp;</label>
<input ng-model="query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
</md-input-container>

141
ui/src/app/entity/entity-subtype-autocomplete.directive.js

@ -0,0 +1,141 @@
/*
* Copyright © 2016-2017 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 './entity-subtype-autocomplete.scss';
/* eslint-disable import/no-unresolved, import/default */
import entitySubtypeAutocompleteTemplate from './entity-subtype-autocomplete.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntitySubtypeAutocomplete($compile, $templateCache, $q, $filter, assetService, deviceService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entitySubtypeAutocompleteTemplate);
element.html(template);
scope.tbRequired = angular.isDefined(scope.tbRequired) ? scope.tbRequired : false;
scope.subType = null;
scope.subTypeSearchText = '';
scope.entitySubtypes = null;
scope.fetchSubTypes = function(searchText) {
var deferred = $q.defer();
loadSubTypes().then(
function success(subTypes) {
var result = $filter('filter')(subTypes, {'$': searchText});
if (result && result.length) {
deferred.resolve(result);
} else {
deferred.resolve([searchText]);
}
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
scope.subTypeSearchTextChanged = function() {
}
scope.updateView = function () {
if (!scope.disabled) {
ngModelCtrl.$setViewValue(scope.subType);
}
}
ngModelCtrl.$render = function () {
scope.subType = ngModelCtrl.$viewValue;
}
scope.$watch('entityType', function () {
load();
});
scope.$watch('subType', function (newValue, prevValue) {
if (!angular.equals(newValue, prevValue)) {
scope.updateView();
}
});
scope.$watch('disabled', function () {
scope.updateView();
});
function loadSubTypes() {
var deferred = $q.defer();
if (!scope.entitySubtypes) {
var entitySubtypesPromise;
if (scope.entityType == types.entityType.asset) {
entitySubtypesPromise = assetService.getAssetTypes();
} else if (scope.entityType == types.entityType.device) {
entitySubtypesPromise = deviceService.getDeviceTypes();
}
if (entitySubtypesPromise) {
entitySubtypesPromise.then(
function success(types) {
scope.entitySubtypes = [];
types.forEach(function (type) {
scope.entitySubtypes.push(type.type);
});
deferred.resolve(scope.entitySubtypes);
},
function fail() {
deferred.reject();
}
);
} else {
deferred.reject();
}
} else {
deferred.resolve(scope.entitySubtypes);
}
return deferred.promise;
}
function load() {
if (scope.entityType == types.entityType.asset) {
scope.selectEntitySubtypeText = 'asset.select-asset-type';
scope.entitySubtypeText = 'asset.asset-type';
scope.entitySubtypeRequiredText = 'asset.asset-type-required';
} else if (scope.entityType == types.entityType.device) {
scope.selectEntitySubtypeText = 'device.select-device-type';
scope.entitySubtypeText = 'device.device-type';
scope.entitySubtypeRequiredText = 'device.device-type-required';
scope.$on('deviceSaved', function() {
scope.entitySubtypes = null;
});
}
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
theForm: '=?',
tbRequired: '=?',
disabled:'=ngDisabled',
entityType: "="
}
};
}

8
ui/src/app/components/device-alias-select.scss → ui/src/app/entity/entity-subtype-autocomplete.scss

@ -13,14 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-device-alias-autocomplete {
.tb-not-found {
.tb-entity-subtype-autocomplete {
.tb-entity-subtype-item {
display: block;
line-height: 1.5;
height: 48px;
.tb-no-entries {
line-height: 48px;
}
}
li {
height: auto !important;

41
ui/src/app/entity/entity-subtype-autocomplete.tpl.html

@ -0,0 +1,41 @@
<!--
Copyright © 2016-2017 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-autocomplete ng-required="tbRequired"
ng-disabled="disabled"
md-no-cache="true"
md-input-name="subType"
ng-model="subType"
md-selected-item="subType"
md-search-text="subTypeSearchText"
md-search-text-change="subTypeSearchTextChanged()"
md-items="item in fetchSubTypes(subTypeSearchText)"
md-item-text="item"
md-min-length="0"
placeholder="{{ selectEntitySubtypeText | translate }}"
md-floating-label="{{ entitySubtypeText | translate }}"
md-select-on-match="true"
md-menu-class="tb-entity-subtype-autocomplete">
<md-item-template>
<div class="tb-entity-subtype-item">
<span md-highlight-text="subTypeSearchText" md-highlight-flags="^i">{{item}}</span>
</div>
</md-item-template>
<div ng-messages="theForm.subType.$error">
<div translate ng-message="required">{{ entitySubtypeRequiredText }}</div>
</div>
</md-autocomplete>

138
ui/src/app/entity/entity-subtype-select.directive.js

@ -0,0 +1,138 @@
/*
* Copyright © 2016-2017 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 './entity-subtype-select.scss';
/* eslint-disable import/no-unresolved, import/default */
import entitySubtypeSelectTemplate from './entity-subtype-select.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function EntitySubtypeSelect($compile, $templateCache, $translate, assetService, deviceService, types) {
var linker = function (scope, element, attrs, ngModelCtrl) {
var template = $templateCache.get(entitySubtypeSelectTemplate);
element.html(template);
if (angular.isDefined(attrs.hideLabel)) {
scope.showLabel = false;
} else {
scope.showLabel = true;
}
scope.ngModelCtrl = ngModelCtrl;
scope.entitySubtypes = [];
scope.subTypeName = function(subType) {
if (subType && subType.length) {
if (scope.typeTranslatePrefix) {
return $translate.instant(scope.typeTranslatePrefix + '.' + subType);
} else {
return subType;
}
} else {
return $translate.instant('entity.all-subtypes');
}
}
scope.$watch('entityType', function () {
load();
});
scope.$watch('entitySubtype', function (newValue, prevValue) {
if (!angular.equals(newValue, prevValue)) {
scope.updateView();
}
});
scope.updateView = function () {
ngModelCtrl.$setViewValue(scope.entitySubtype);
};
ngModelCtrl.$render = function () {
scope.entitySubtype = ngModelCtrl.$viewValue;
};
function loadSubTypes() {
scope.entitySubtypes.length = 0;
var entitySubtypesPromise;
if (scope.entityType == types.entityType.asset) {
entitySubtypesPromise = assetService.getAssetTypes();
} else if (scope.entityType == types.entityType.device) {
entitySubtypesPromise = deviceService.getDeviceTypes();
}
if (entitySubtypesPromise) {
entitySubtypesPromise.then(
function success(types) {
scope.entitySubtypes.push('');
types.forEach(function(type) {
scope.entitySubtypes.push(type.type);
});
if (scope.entitySubtypes.indexOf(scope.entitySubtype) == -1) {
scope.entitySubtype = '';
}
},
function fail() {}
);
}
}
function load() {
if (scope.entityType == types.entityType.asset) {
scope.entitySubtypeTitle = 'asset.asset-type';
scope.entitySubtypeRequiredText = 'asset.asset-type-required';
} else if (scope.entityType == types.entityType.device) {
scope.entitySubtypeTitle = 'device.device-type';
scope.entitySubtypeRequiredText = 'device.device-type-required';
}
scope.entitySubtypes.length = 0;
if (scope.entitySubtypesList && scope.entitySubtypesList.length) {
scope.entitySubtypesList.forEach(function(subType) {
scope.entitySubtypes.push(subType);
});
} else {
loadSubTypes();
if (scope.entityType == types.entityType.asset) {
scope.$on('assetSaved', function() {
loadSubTypes();
});
} else if (scope.entityType == types.entityType.device) {
scope.$on('deviceSaved', function() {
loadSubTypes();
});
}
}
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
require: "^ngModel",
link: linker,
scope: {
theForm: '=?',
entityType: "=",
entitySubtypesList: "=?",
typeTranslatePrefix: "@?"
}
};
}

10
ui/src/app/dashboard/device-aliases.scss → ui/src/app/entity/entity-subtype-select.scss

@ -14,12 +14,6 @@
* limitations under the License.
*/
.tb-aliases-dialog {
.md-dialog-content {
padding-bottom: 0px;
}
.tb-alias {
padding: 10px 0 0 10px;
margin: 5px;
}
md-select.tb-entity-subtype-select {
min-width: 200px;
}

28
ui/src/app/entity/entity-subtype-select.tpl.html

@ -0,0 +1,28 @@
<!--
Copyright © 2016-2017 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-input-container>
<label ng-if="showLabel">{{ entitySubtypeTitle | translate }}</label>
<md-select name="subType" ng-model="entitySubtype" class="tb-entity-subtype-select" aria-label="{{ entitySubtypeTitle | translate }}">
<md-option ng-repeat="subType in entitySubtypes" ng-value="subType">
{{ subTypeName(subType) }}
</md-option>
</md-select>
<div ng-messages="theForm.subType.$error">
<div ng-message="required" translate>{{ entitySubtypeRequiredText }}</div>
</div>
</md-input-container>

4
ui/src/app/entity/index.js

@ -16,6 +16,8 @@
import EntityAliasesController from './entity-aliases.controller';
import EntityTypeSelectDirective from './entity-type-select.directive';
import EntitySubtypeSelectDirective from './entity-subtype-select.directive';
import EntitySubtypeAutocompleteDirective from './entity-subtype-autocomplete.directive';
import EntityFilterDirective from './entity-filter.directive';
import AliasesEntitySelectPanelController from './aliases-entity-select-panel.controller';
import AliasesEntitySelectDirective from './aliases-entity-select.directive';
@ -29,6 +31,8 @@ export default angular.module('thingsboard.entity', [])
.controller('AddAttributeDialogController', AddAttributeDialogController)
.controller('AddWidgetToDashboardDialogController', AddWidgetToDashboardDialogController)
.directive('tbEntityTypeSelect', EntityTypeSelectDirective)
.directive('tbEntitySubtypeSelect', EntitySubtypeSelectDirective)
.directive('tbEntitySubtypeAutocomplete', EntitySubtypeAutocompleteDirective)
.directive('tbEntityFilter', EntityFilterDirective)
.directive('tbAliasesEntitySelect', AliasesEntitySelectDirective)
.directive('tbAttributeTable', AttributeTableDirective)

37
ui/src/app/layout/home.controller.js

@ -25,7 +25,7 @@ import logoSvg from '../../svg/logo_title_white.svg';
/* eslint-disable angular/angularelement */
/*@ngInject*/
export default function HomeController(loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state,
export default function HomeController(types, loginService, userService, deviceService, Fullscreen, $scope, $element, $rootScope, $document, $state,
$window, $log, $mdMedia, $animate, $timeout) {
var siteSideNav = $('.tb-site-sidenav', $element);
@ -38,8 +38,11 @@ export default function HomeController(loginService, userService, deviceService,
if (angular.isUndefined($rootScope.searchConfig)) {
$rootScope.searchConfig = {
searchEnabled: false,
searchByEntitySubtype: false,
searchEntityType: null,
showSearch: false,
searchText: ""
searchText: "",
searchEntitySubtype: ""
};
}
@ -47,6 +50,7 @@ export default function HomeController(loginService, userService, deviceService,
vm.isLockSidenav = false;
vm.displaySearchMode = displaySearchMode;
vm.displayEntitySubtypeSearch = displayEntitySubtypeSearch;
vm.openSidenav = openSidenav;
vm.goBack = goBack;
vm.searchTextUpdated = searchTextUpdated;
@ -54,25 +58,35 @@ export default function HomeController(loginService, userService, deviceService,
vm.toggleFullscreen = toggleFullscreen;
$scope.$on('$stateChangeSuccess', function (evt, to, toParams, from) {
watchEntitySubtype(false);
if (angular.isDefined(to.data.searchEnabled)) {
$scope.searchConfig.searchEnabled = to.data.searchEnabled;
$scope.searchConfig.searchByEntitySubtype = to.data.searchByEntitySubtype;
$scope.searchConfig.searchEntityType = to.data.searchEntityType;
if ($scope.searchConfig.searchEnabled === false || to.name !== from.name) {
$scope.searchConfig.showSearch = false;
$scope.searchConfig.searchText = "";
$scope.searchConfig.searchEntitySubtype = "";
}
} else {
$scope.searchConfig.searchEnabled = false;
$scope.searchConfig.searchByEntitySubtype = false;
$scope.searchConfig.searchEntityType = null;
$scope.searchConfig.showSearch = false;
$scope.searchConfig.searchText = "";
$scope.searchConfig.searchEntitySubtype = "";
}
watchEntitySubtype($scope.searchConfig.searchByEntitySubtype);
});
if ($mdMedia('gt-sm')) {
vm.isGtSm = $mdMedia('gt-sm');
if (vm.isGtSm) {
vm.isLockSidenav = true;
$animate.enabled(siteSideNav, false);
}
$scope.$watch(function() { return $mdMedia('gt-sm'); }, function(isGtSm) {
vm.isGtSm = isGtSm;
vm.isLockSidenav = isGtSm;
vm.isShowSidenav = isGtSm;
if (!isGtSm) {
@ -84,11 +98,28 @@ export default function HomeController(loginService, userService, deviceService,
}
});
function watchEntitySubtype(enableWatch) {
if ($scope.entitySubtypeWatch) {
$scope.entitySubtypeWatch();
}
if (enableWatch) {
$scope.entitySubtypeWatch = $scope.$watch('searchConfig.searchEntitySubtype', function (newVal, prevVal) {
if (!angular.equals(newVal, prevVal)) {
$scope.$broadcast('searchEntitySubtypeUpdated');
}
});
}
}
function displaySearchMode() {
return $scope.searchConfig.searchEnabled &&
$scope.searchConfig.showSearch;
}
function displayEntitySubtypeSearch() {
return $scope.searchConfig.searchByEntitySubtype && vm.isGtSm;
}
function toggleFullscreen() {
if (Fullscreen.isEnabled()) {
Fullscreen.cancel();

8
ui/src/app/layout/home.scss

@ -70,3 +70,11 @@ md-icon.tb-logo-title {
z-index: 2;
white-space: nowrap;
}
.tb-entity-subtype-search {
margin-top: 15px;
}
.tb-entity-search {
margin-top: 34px;
}

18
ui/src/app/layout/home.tpl.html

@ -39,7 +39,7 @@
</md-sidenav>
<div flex layout="column" tabIndex="-1" role="main">
<md-toolbar class="md-whiteframe-z1 tb-primary-toolbar" ng-class="{'md-hue-1': vm.displaySearchMode()}">
<md-toolbar class="md-whiteframe-z1 tb-primary-toolbar">
<div layout="row" flex class="md-toolbar-tools">
<md-button id="main" hide-gt-sm ng-show="!forceFullscreen"
class="md-icon-button" ng-click="vm.openSidenav()" aria-label="{{ 'home.menu' | translate }}" ng-class="{'tb-invisible': vm.displaySearchMode()}">
@ -55,10 +55,18 @@
<div flex layout="row" ng-show="!vm.displaySearchMode()" tb-no-animate class="md-toolbar-tools">
<span ng-cloak ncy-breadcrumb></span>
</div>
<md-input-container ng-show="vm.displaySearchMode()" md-theme="tb-search-input" flex>
<label>&nbsp;</label>
<input ng-model="searchConfig.searchText" ng-change="vm.searchTextUpdated()" placeholder="{{ 'common.enter-search' | translate }}"/>
</md-input-container>
<div layout="row" ng-show="vm.displaySearchMode()" md-theme="tb-dark" flex>
<div class="tb-entity-subtype-search" layout="row" layout-align="start center" ng-if="vm.displayEntitySubtypeSearch()">
<tb-entity-subtype-select
entity-type="searchConfig.searchEntityType"
ng-model="searchConfig.searchEntitySubtype">
</tb-entity-subtype-select>
</div>
<md-input-container ng-class="{'tb-entity-search': vm.displayEntitySubtypeSearch()}" flex>
<label>&nbsp;</label>
<input ng-model="searchConfig.searchText" ng-change="vm.searchTextUpdated()" placeholder="{{ 'common.enter-search' | translate }}"/>
</md-input-container>
</div>
<md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}" ng-show="searchConfig.searchEnabled" ng-click="searchConfig.showSearch = !searchConfig.showSearch">
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
</md-button>

6
ui/src/app/layout/user-menu.directive.js

@ -48,16 +48,10 @@ function UserMenuController($scope, userService, $translate, $state) {
var dashboardUser = userService.getCurrentUser();
vm.authorityName = authorityName;
vm.displaySearchMode = displaySearchMode;
vm.logout = logout;
vm.openProfile = openProfile;
vm.userDisplayName = userDisplayName;
function displaySearchMode() {
return $scope.searchConfig.searchEnabled &&
$scope.searchConfig.showSearch;
}
function authorityName() {
var name = "user.anonymous";
if (dashboardUser) {

7
ui/src/app/locale/locale.constant.js

@ -124,6 +124,9 @@ export default angular.module('thingsboard.locale', [])
"unassign-from-customer": "Unassign from customer",
"delete": "Delete asset",
"asset-public": "Asset is public",
"asset-type": "Asset type",
"asset-type-required": "Asset type is required.",
"select-asset-type": "Select asset type",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
@ -477,6 +480,9 @@ export default angular.module('thingsboard.locale', [])
"rsa-key-required": "RSA public key is required.",
"secret": "Secret",
"secret-required": "Secret is required.",
"device-type": "Device type",
"device-type-required": "Device type is required.",
"select-device-type": "Select device type",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
@ -521,6 +527,7 @@ export default angular.module('thingsboard.locale', [])
"entity-list-empty": "No entities selected.",
"entity-name-filter-required": "Entity name filter is required.",
"entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
"all-subtypes": "All",
"type": "Type",
"type-device": "Device",
"type-asset": "Asset",

Loading…
Cancel
Save