From d69e98e8b9229509796e1d1f85e011d34c9f6019 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 19 Jun 2017 19:22:48 +0300 Subject: [PATCH] TB-62: Implement Entities table widget --- ui/src/app/api/widget.service.js | 28 +- .../components/datasource-entity.directive.js | 13 +- .../app/components/datasource-entity.tpl.html | 5 + .../components/datasource-func.directive.js | 7 +- .../app/components/datasource-func.tpl.html | 7 +- ui/src/app/components/datasource.directive.js | 1 + ui/src/app/components/datasource.tpl.html | 2 + .../app/components/widget-config.directive.js | 1 + ui/src/app/components/widget-config.tpl.html | 14 +- ui/src/app/components/widget.controller.js | 13 +- ui/src/app/dashboard/add-widget.tpl.html | 1 + ui/src/app/dashboard/dashboard.controller.js | 2 +- ui/src/app/dashboard/dashboard.tpl.html | 5 + ui/src/app/dashboard/edit-widget.directive.js | 3 +- ui/src/app/dashboard/edit-widget.tpl.html | 1 + ui/src/app/locale/locale.constant.js | 13 +- .../app/widget/lib/entities-table-widget.js | 474 ++++++++++++++++++ .../app/widget/lib/entities-table-widget.scss | 58 +++ .../widget/lib/entities-table-widget.tpl.html | 86 ++++ .../app/widget/widget-library.controller.js | 14 +- ui/src/app/widget/widget-library.tpl.html | 1 + 21 files changed, 725 insertions(+), 24 deletions(-) create mode 100644 ui/src/app/widget/lib/entities-table-widget.js create mode 100644 ui/src/app/widget/lib/entities-table-widget.scss create mode 100644 ui/src/app/widget/lib/entities-table-widget.tpl.html diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js index 1c5e5f4126..f8f27ed932 100644 --- a/ui/src/app/api/widget.service.js +++ b/ui/src/app/api/widget.service.js @@ -20,6 +20,7 @@ import tinycolor from 'tinycolor2'; import thingsboardLedLight from '../components/led-light.directive'; import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget'; +import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget'; import TbFlot from '../widget/lib/flot-widget'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; @@ -34,7 +35,7 @@ import thingsboardTypes from '../common/types.constant'; import thingsboardUtils from '../common/utils.service'; export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, - thingsboardAlarmsTableWidget, thingsboardTypes, thingsboardUtils]) + thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardTypes, thingsboardUtils]) .factory('widgetService', WidgetService) .name; @@ -546,6 +547,14 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ ' }\n\n' + + ' self.typeParameters = function() {\n\n' + + { + useCustomDatasources: false, + maxDatasources: -1 //unlimited + maxDataKeys: -1 //unlimited + } + ' }\n\n' + + ' self.onResize = function() {\n\n' + ' }\n\n' + @@ -586,10 +595,21 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); } + if (angular.isFunction(widgetTypeInstance.typeParameters)) { + result.typeParameters = widgetTypeInstance.typeParameters(); + } else { + result.typeParameters = {}; + } if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) { - result.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); + result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); } else { - result.useCustomDatasources = false; + result.typeParameters.useCustomDatasources = false; + } + if (angular.isUndefined(result.typeParameters.maxDatasources)) { + result.typeParameters.maxDatasources = -1; + } + if (angular.isUndefined(result.typeParameters.maxDataKeys)) { + result.typeParameters.maxDataKeys = -1; } return result; } catch (e) { @@ -629,7 +649,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ if (widgetType.dataKeySettingsSchema) { widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema; } - widgetInfo.useCustomDatasources = widgetType.useCustomDatasources; + widgetInfo.typeParameters = widgetType.typeParameters; putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem); deferred.resolve(widgetInfo); diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js index e6ec8f83a6..46c8b6344f 100644 --- a/ui/src/app/components/datasource-entity.directive.js +++ b/ui/src/app/components/datasource-entity.directive.js @@ -156,11 +156,19 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc }; scope.transformTimeseriesDataKeyChip = function (chip) { - return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries}); + if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) { + return null; + } else { + return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries}); + } }; scope.transformAttributeDataKeyChip = function (chip) { - return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute}); + if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) { + return null; + } else { + return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute}); + } }; scope.transformAlarmDataKeyChip = function (chip) { @@ -272,6 +280,7 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc require: "^ngModel", scope: { widgetType: '=', + maxDataKeys: '=', aliasController: '=', datakeySettingsSchema: '=', generateDataKey: '&', diff --git a/ui/src/app/components/datasource-entity.tpl.html b/ui/src/app/components/datasource-entity.tpl.html index 9876491086..e788f30949 100644 --- a/ui/src/app/components/datasource-entity.tpl.html +++ b/ui/src/app/components/datasource-entity.tpl.html @@ -186,5 +186,10 @@
datakey.timeseries-or-attributes-required
datakey.alarm-fields-required
+
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js index 2a2ae82111..96e03871c1 100644 --- a/ui/src/app/components/datasource-func.directive.js +++ b/ui/src/app/components/datasource-func.directive.js @@ -117,7 +117,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, }; scope.transformFuncDataKeyChip = function (chip) { - return scope.generateDataKey({chip: chip, type: types.dataKeyType.function}); + if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) { + return null; + } else { + return scope.generateDataKey({chip: chip, type: types.dataKeyType.function}); + } }; scope.transformAlarmDataKeyChip = function (chip) { @@ -217,6 +221,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document, require: "^ngModel", scope: { widgetType: '=', + maxDataKeys: '=', generateDataKey: '&', datakeySettingsSchema: '=' }, diff --git a/ui/src/app/components/datasource-func.tpl.html b/ui/src/app/components/datasource-func.tpl.html index 7fa4aac008..8a57b07699 100644 --- a/ui/src/app/components/datasource-func.tpl.html +++ b/ui/src/app/components/datasource-func.tpl.html @@ -17,7 +17,7 @@ -->
- datakey.function-types-required
datakey.alarm-fields-required
+
diff --git a/ui/src/app/components/datasource.directive.js b/ui/src/app/components/datasource.directive.js index b5327e298c..42011b03a4 100644 --- a/ui/src/app/components/datasource.directive.js +++ b/ui/src/app/components/datasource.directive.js @@ -82,6 +82,7 @@ function Datasource($compile, $templateCache, utils, types) { require: "^ngModel", scope: { aliasController: '=', + maxDataKeys: '=', widgetType: '=', functionsOnly: '=', datakeySettingsSchema: '=', diff --git a/ui/src/app/components/datasource.tpl.html b/ui/src/app/components/datasource.tpl.html index 0633f5f2bf..41b18dbb51 100644 --- a/ui/src/app/components/datasource.tpl.html +++ b/ui/src/app/components/datasource.tpl.html @@ -27,6 +27,7 @@ +
{{ 'widget-config.datasources' | translate }}
+
+
@@ -88,6 +95,7 @@ style="padding: 0 0 0 10px; margin: 5px;">
- {{ 'widget-config.add-datasource' | translate }} @@ -140,7 +148,7 @@ - diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js index baedd094b0..f2e231fa29 100644 --- a/ui/src/app/components/widget.controller.js +++ b/ui/src/app/components/widget.controller.js @@ -128,7 +128,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q var widgetTypeInstance; - vm.useCustomDatasources = false; + vm.typeParameters = widgetInfo.typeParameters; try { widgetTypeInstance = new widgetType(widgetContext); @@ -154,9 +154,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q if (!widgetTypeInstance.onDestroy) { widgetTypeInstance.onDestroy = function() {}; } - if (widgetTypeInstance.useCustomDatasources) { - vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); - } //TODO: widgets visibility @@ -502,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q var subscription = widgetContext.subscriptions[id]; subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds); } - if (subscriptionChanged && !vm.useCustomDatasources) { + if (subscriptionChanged && !vm.typeParameters.useCustomDatasources) { reInit(); } }); @@ -513,7 +510,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q configureWidgetElement(); var deferred = $q.defer(); - if (!vm.useCustomDatasources) { + if (!vm.typeParameters.useCustomDatasources) { createDefaultSubscription().then( function success() { subscriptionInited = true; @@ -535,7 +532,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q function reInit() { onDestroy(); configureWidgetElement(); - if (!vm.useCustomDatasources) { + if (!vm.typeParameters.useCustomDatasources) { createDefaultSubscription().then( function success() { subscriptionInited = true; @@ -575,7 +572,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q } catch (e) { handleWidgetException(e); } - if (!vm.useCustomDatasources && widgetContext.defaultSubscription) { + if (!vm.typeParameters.useCustomDatasources && widgetContext.defaultSubscription) { widgetContext.defaultSubscription.subscribe(); } } diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html index 761776057b..894c23d482 100644 --- a/ui/src/app/dashboard/add-widget.tpl.html +++ b/ui/src/app/dashboard/add-widget.tpl.html @@ -33,6 +33,7 @@
0) { + vm.defaultPageSize = pageSize; + } + + if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) { + vm.defaultSortOrder = vm.settings.defaultSortOrder; + } + + vm.query.order = vm.defaultSortOrder; + vm.query.limit = vm.defaultPageSize; + if (vm.isGtMd) { + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3]; + } else { + vm.limitOptions = null; + } + + var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)'; + var defaultColor = tinycolor(origColor); + var mdDark = defaultColor.setAlpha(0.87).toRgbString(); + var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString(); + var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString(); + //var mdDarkIcon = mdDarkSecondary; + var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString(); + + var cssString = 'table.md-table th.md-column {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + 'table.md-table th.md-column.md-checkbox-column md-checkbox:not(.md-checked) .md-icon {\n'+ + 'border-color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + 'table.md-table th.md-column md-icon.md-sort-icon {\n'+ + 'color: ' + mdDarkDisabled + ';\n'+ + '}\n'+ + 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+ + 'color: ' + mdDark + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell {\n'+ + 'color: ' + mdDark + ';\n'+ + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ + '}\n'+ + 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+ + 'border-color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell.md-placeholder {\n'+ + 'color: ' + mdDarkDisabled + ';\n'+ + '}\n'+ + 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + '.md-table-pagination {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + 'border-top: 1px '+mdDarkDivider+' solid;\n'+ + '}\n'+ + '.md-table-pagination .buttons md-icon {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}\n'+ + '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+ + 'color: ' + mdDarkSecondary + ';\n'+ + '}'; + + var cssParser = new cssjs(); + cssParser.testMode = false; + var namespace = 'entities-table-' + hashCode(cssString); + cssParser.cssPreviewNamespace = namespace; + cssParser.createStyleElement(namespace, cssString); + $element.addClass(namespace); + + function hashCode(str) { + var hash = 0; + var i, char; + if (str.length === 0) return hash; + for (i = 0; i < str.length; i++) { + char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return hash; + } + } + + function enterFilterMode () { + vm.query.search = ''; + vm.ctx.hideTitlePanel = true; + } + + function exitFilterMode () { + vm.query.search = null; + updateEntities(); + vm.ctx.hideTitlePanel = false; + } + + function onReorder () { + updateEntities(); + } + + function onPaginate () { + updateEntities(); + } + + function onRowClick($event, entity) { + if (vm.currentEntity != entity) { + vm.currentEntity = entity; + } + } + + function isCurrent(entity) { + return (vm.currentEntity && entity && vm.currentEntity.id && entity.id) && + (vm.currentEntity.id.id === entity.id.id); + } + + function updateEntities() { + var result = $filter('orderBy')(vm.allEntities, vm.query.order); + if (vm.query.search != null) { + result = $filter('filter')(result, {$: vm.query.search}); + } + vm.entitiesCount = result.length; + + if (vm.displayPagination) { + var startIndex = vm.query.limit * (vm.query.page - 1); + vm.entities = result.slice(startIndex, startIndex + vm.query.limit); + } else { + vm.entities = result; + } + } + + function cellStyle(entity, key) { + var style = {}; + if (entity && key) { + var styleInfo = vm.stylesInfo[key.label]; + var value = getEntityValue(entity, key); + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) { + try { + style = styleInfo.cellStyleFunction(value); + } catch (e) { + style = {}; + } + } else { + style = defaultStyle(key, value); + } + } + if (!style.width) { + var columnWidth = vm.columnWidth[key.label]; + style.width = columnWidth; + } + return style; + } + + function cellContent(entity, key) { + var strContent = ''; + if (entity && key) { + var contentInfo = vm.contentsInfo[key.label]; + var value = getEntityValue(entity, key); + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) { + if (angular.isDefined(value)) { + strContent = '' + value; + } + var content = strContent; + try { + content = contentInfo.cellContentFunction(value, entity, $filter); + } catch (e) { + content = strContent; + } + } else { + content = defaultContent(key, value); + } + return content; + } else { + return strContent; + } + } + + function defaultContent(key, value) { + if (angular.isDefined(value)) { + return value; + } else { + return ''; + } + } + + function defaultStyle(/*key, value*/) { + return {}; + } + + const getDescendantProp = (obj, path) => ( + path.split('.').reduce((acc, part) => acc && acc[part], obj) + ); + + function getEntityValue(entity, key) { + return getDescendantProp(entity, key.name); + } + + function updateEntitiesData(data) { + if (vm.allEntities) { + for (var i=0;i 1) { + var value = keyData[0][1]; + entity[dataKey.name] = value; + } else { + entity[dataKey.name] = ''; + } + } + } + } + } + + function updateDatasources() { + + vm.stylesInfo = {}; + vm.contentsInfo = {}; + vm.columnWidth = {}; + vm.dataKeys = []; + vm.allEntities = []; + + var datasource; + var dataKey; + + datasource = vm.datasources[0]; + + vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.entitiesTitle); + + for (var d = 0; d < datasource.dataKeys.length; d++ ) { + dataKey = angular.copy(datasource.dataKeys[d]); + if (dataKey.type == types.dataKeyType.function) { + dataKey.name = dataKey.label; + } + vm.dataKeys.push(dataKey); + + var translationId = types.translate.customTranslationsPrefix + dataKey.label; + var translation = $translate.instant(translationId); + if (translation != translationId) { + dataKey.title = translation + ''; + } else { + dataKey.title = dataKey.label; + } + + var keySettings = dataKey.settings; + + var cellStyleFunction = null; + var useCellStyleFunction = false; + + if (keySettings.useCellStyleFunction === true) { + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) { + try { + cellStyleFunction = new Function('value', keySettings.cellStyleFunction); + useCellStyleFunction = true; + } catch (e) { + cellStyleFunction = null; + useCellStyleFunction = false; + } + } + } + + vm.stylesInfo[dataKey.label] = { + useCellStyleFunction: useCellStyleFunction, + cellStyleFunction: cellStyleFunction + }; + + var cellContentFunction = null; + var useCellContentFunction = false; + + if (keySettings.useCellContentFunction === true) { + if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) { + try { + cellContentFunction = new Function('value, entity, filter', keySettings.cellContentFunction); + useCellContentFunction = true; + } catch (e) { + cellContentFunction = null; + useCellContentFunction = false; + } + } + } + + vm.contentsInfo[dataKey.label] = { + useCellContentFunction: useCellContentFunction, + cellContentFunction: cellContentFunction + }; + + var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px'; + vm.columnWidth[dataKey.label] = columnWidth; + } + + for (var i=0;i +
+
+ +
+ + search + + {{'entity.search' | translate}} + + + + + + + + close + + {{ 'action.close' | translate }} + + +
+
+ + + + + + + + + + + + + + + + + + +
entity.entity-nameentity.entity-type{{ key.title }} 
{{entity.entityName}}{{entity.entityType}} + + +
+ + entity.no-entities-prompt +
+ + +
+ entity.no-data +
diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js index bda8835fb8..798227f05d 100644 --- a/ui/src/app/widget/widget-library.controller.js +++ b/ui/src/app/widget/widget-library.controller.js @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import AliasController from '../api/alias-controller'; + /* eslint-disable import/no-unresolved, import/default */ import selectWidgetTypeTemplate from './select-widget-type.tpl.html'; @@ -21,7 +24,8 @@ import selectWidgetTypeTemplate from './select-widget-type.tpl.html'; /*@ngInject*/ export default function WidgetLibraryController($scope, $rootScope, $q, widgetService, userService, importExport, - $state, $stateParams, $document, $mdDialog, $translate, $filter, types) { + $state, $stateParams, $document, $mdDialog, $translate, $filter, + utils, types, entityService) { var vm = this; @@ -31,6 +35,14 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe vm.widgetTypes = []; vm.dashboardInitComplete = false; + var stateController = { + getStateParams: function() { + return {}; + } + }; + vm.aliasController = new AliasController($scope, $q, $filter, utils, + types, entityService, stateController, {}); + vm.noData = noData; vm.dashboardInited = dashboardInited; vm.dashboardInitFailed = dashboardInitFailed; diff --git a/ui/src/app/widget/widget-library.tpl.html b/ui/src/app/widget/widget-library.tpl.html index bd3e0c33f5..b2dd4a782d 100644 --- a/ui/src/app/widget/widget-library.tpl.html +++ b/ui/src/app/widget/widget-library.tpl.html @@ -28,6 +28,7 @@ class="md-headline tb-absolute-fill">widgets-bundle.empty