thingsboard-memoizeit
Changes
ui/src/app/api/widget.service.js 28(+24 -4)
ui/src/app/components/widget-config.tpl.html 14(+11 -3)
ui/src/app/locale/locale.constant.js 13(+11 -2)
ui/src/app/widget/lib/entities-table-widget.js 474(+474 -0)
Details
ui/src/app/api/widget.service.js 28(+24 -4)
diff --git a/ui/src/app/api/widget.service.js b/ui/src/app/api/widget.service.js
index 1c5e5f4..f8f27ed 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.directive.js b/ui/src/app/components/datasource.directive.js
index b5327e2..42011b0 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 0633f5f..41b18db 100644
--- a/ui/src/app/components/datasource.tpl.html
+++ b/ui/src/app/components/datasource.tpl.html
@@ -27,6 +27,7 @@
<tb-datasource-func flex
ng-switch-default
ng-model="model"
+ max-data-keys="maxDataKeys"
datakey-settings-schema="datakeySettingsSchema"
ng-required="model.type === types.datasourceType.function"
widget-type="widgetType"
@@ -34,6 +35,7 @@
</tb-datasource-func>
<tb-datasource-entity flex
ng-model="model"
+ max-data-keys="maxDataKeys"
datakey-settings-schema="datakeySettingsSchema"
ng-switch-when="entity"
ng-required="model.type === types.datasourceType.entity"
diff --git a/ui/src/app/components/datasource-entity.directive.js b/ui/src/app/components/datasource-entity.directive.js
index e6ec8f8..46c8b63 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 9876491..e788f30 100644
--- a/ui/src/app/components/datasource-entity.tpl.html
+++ b/ui/src/app/components/datasource-entity.tpl.html
@@ -186,5 +186,10 @@
<div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
<div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
</div>
+ <div class="md-caption" style="color: rgba(0,0,0,0.57);" ng-if="maxDataKeys != -1"
+ translate="datakey.maximum-timeseries-or-attributes"
+ translate-values="{count: maxDataKeys}"
+ translate-interpolation="messageformat"
+ ></div>
</section>
</section>
diff --git a/ui/src/app/components/datasource-func.directive.js b/ui/src/app/components/datasource-func.directive.js
index 2a2ae82..96e0387 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 7fa4aac..8a57b07 100644
--- a/ui/src/app/components/datasource-func.tpl.html
+++ b/ui/src/app/components/datasource-func.tpl.html
@@ -17,7 +17,7 @@
-->
<section class="tb-datasource-func" flex layout='column'
layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
- <md-input-container ng-if="widgetType != types.widgetType.alarm.value"
+ <md-input-container ng-show="widgetType != types.widgetType.alarm.value"
class="tb-datasource-name" md-no-float style="min-width: 200px;">
<input name="datasourceName"
placeholder="{{ 'datasource.name' | translate }}"
@@ -132,5 +132,10 @@
<div translate ng-message="datasourceKeys" ng-if="widgetType !== types.widgetType.alarm.value" class="tb-error-message">datakey.function-types-required</div>
<div translate ng-message="datasourceKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
</div>
+ <div class="md-caption" style="color: rgba(0,0,0,0.57);" ng-if="maxDataKeys != -1"
+ translate="datakey.maximum-function-types"
+ translate-values="{count: maxDataKeys}"
+ translate-interpolation="messageformat"
+ ></div>
</section>
</section>
diff --git a/ui/src/app/components/widget.controller.js b/ui/src/app/components/widget.controller.js
index baedd09..f2e231f 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/components/widget-config.directive.js b/ui/src/app/components/widget-config.directive.js
index 3849cd3..4a083c1 100644
--- a/ui/src/app/components/widget-config.directive.js
+++ b/ui/src/app/components/widget-config.directive.js
@@ -442,6 +442,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
forceExpandDatasources: '=?',
isDataEnabled: '=?',
widgetType: '=',
+ typeParameters: '=',
widgetSettingsSchema: '=',
datakeySettingsSchema: '=',
aliasController: '=',
ui/src/app/components/widget-config.tpl.html 14(+11 -3)
diff --git a/ui/src/app/components/widget-config.tpl.html b/ui/src/app/components/widget-config.tpl.html
index 4bfe55c..14cb27c 100644
--- a/ui/src/app/components/widget-config.tpl.html
+++ b/ui/src/app/components/widget-config.tpl.html
@@ -62,7 +62,14 @@
&& isDataEnabled">
<v-pane id="datasources-pane" expanded="true">
<v-pane-header>
- {{ 'widget-config.datasources' | translate }}
+ <div layout="column">
+ <div>{{ 'widget-config.datasources' | translate }}</div>
+ <div class="md-caption" style="color: rgba(0,0,0,0.57);" ng-if="typeParameters.maxDatasources != -1"
+ translate="widget-config.maximum-datasources"
+ translate-values="{count: typeParameters.maxDatasources}"
+ translate-interpolation="messageformat"
+ ></div>
+ </div>
</v-pane-header>
<v-pane-content>
<div ng-if="datasources.length === 0">
@@ -88,6 +95,7 @@
style="padding: 0 0 0 10px; margin: 5px;">
<tb-datasource flex ng-model="datasource.value"
widget-type="widgetType"
+ max-data-keys="typeParameters.maxDataKeys"
alias-controller="aliasController"
functions-only="functionsOnly"
datakey-settings-schema="datakeySettingsSchema"
@@ -111,7 +119,7 @@
</div>
</div>
<div flex layout="row" layout-align="start center">
- <md-button ng-disabled="loading" class="md-primary md-raised"
+ <md-button ng-show="typeParameters.maxDatasources == -1 || datasources.length < typeParameters.maxDatasources" ng-disabled="loading" class="md-primary md-raised"
ng-click="addDatasource($event)" aria-label="{{ 'action.add' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget-config.add-datasource' | translate }}
@@ -140,7 +148,7 @@
</v-pane-content>
</v-pane>
</v-accordion>
- <v-accordion id="alarn-source-accordion" control="alarmSourceAccordion" class="vAccordion--default"
+ <v-accordion id="alarm-source-accordion" control="alarmSourceAccordion" class="vAccordion--default"
ng-if="widgetType === types.widgetType.alarm.value && isDataEnabled">
<v-pane id="alarm-source-pane" expanded="true">
<v-pane-header>
diff --git a/ui/src/app/dashboard/add-widget.tpl.html b/ui/src/app/dashboard/add-widget.tpl.html
index 7617760..894c23d 100644
--- a/ui/src/app/dashboard/add-widget.tpl.html
+++ b/ui/src/app/dashboard/add-widget.tpl.html
@@ -33,6 +33,7 @@
<div class="md-dialog-content" style="padding-top: 0px;">
<fieldset ng-disabled="loading" style="position: relative; height: 600px;">
<tb-widget-config widget-type="vm.widget.type"
+ type-parameters="vm.widgetInfo.typeParameters"
force-expand-datasources="true"
ng-model="vm.widgetConfig"
widget-settings-schema="vm.settingsSchema"
diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js
index 735c881..f41a635 100644
--- a/ui/src/app/dashboard/dashboard.controller.js
+++ b/ui/src/app/dashboard/dashboard.controller.js
@@ -918,7 +918,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
}
}
- if (widgetTypeInfo.useCustomDatasources) {
+ if (widgetTypeInfo.typeParameters.useCustomDatasources) {
addWidget(newWidget);
} else {
$mdDialog.show({
diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html
index 1a3b9bf..72820bb 100644
--- a/ui/src/app/dashboard/dashboard.tpl.html
+++ b/ui/src/app/dashboard/dashboard.tpl.html
@@ -212,6 +212,7 @@
class="tb-absolute-fill" md-border-bottom>
<md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
<tb-dashboard
+ alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.timeseriesWidgetTypes"
is-edit="false"
is-mobile="true"
@@ -222,6 +223,7 @@
</md-tab>
<md-tab ng-if="vm.latestWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">
<tb-dashboard
+ alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.latestWidgetTypes"
is-edit="false"
is-mobile="true"
@@ -232,6 +234,7 @@
</md-tab>
<md-tab ng-if="vm.rpcWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">
<tb-dashboard
+ alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.rpcWidgetTypes"
is-edit="false"
is-mobile="true"
@@ -242,6 +245,7 @@
</md-tab>
<md-tab ng-if="vm.alarmWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">
<tb-dashboard
+ alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.alarmWidgetTypes"
is-edit="false"
is-mobile="true"
@@ -252,6 +256,7 @@
</md-tab>
<md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
<tb-dashboard
+ alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.staticWidgetTypes"
is-edit="false"
is-mobile="true"
diff --git a/ui/src/app/dashboard/edit-widget.directive.js b/ui/src/app/dashboard/edit-widget.directive.js
index d0c651a..dcf05e2 100644
--- a/ui/src/app/dashboard/edit-widget.directive.js
+++ b/ui/src/app/dashboard/edit-widget.directive.js
@@ -40,7 +40,8 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
};
var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
- scope.isDataEnabled = !widgetInfo.useCustomDatasources;
+ scope.typeParameters = widgetInfo.typeParameters;
+ scope.isDataEnabled = !widgetInfo.typeParameters.useCustomDatasources;
if (!settingsSchema || settingsSchema === '') {
scope.settingsSchema = {};
} else {
diff --git a/ui/src/app/dashboard/edit-widget.tpl.html b/ui/src/app/dashboard/edit-widget.tpl.html
index 4b4382e..74f5755 100644
--- a/ui/src/app/dashboard/edit-widget.tpl.html
+++ b/ui/src/app/dashboard/edit-widget.tpl.html
@@ -17,6 +17,7 @@
-->
<fieldset ng-disabled="loading">
<tb-widget-config widget-type="widget.type"
+ type-parameters="typeParameters"
ng-model="widgetConfig"
is-data-enabled="isDataEnabled"
widget-settings-schema="settingsSchema"
ui/src/app/locale/locale.constant.js 13(+11 -2)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index b213746..4710b92 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -499,9 +499,11 @@ export default angular.module('thingsboard.locale', [])
"alarm": "Alarm fields",
"timeseries-required": "Entity timeseries are required.",
"timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
+ "maximum-timeseries-or-attributes": "Maximum { count, select, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }",
"alarm-fields-required": "Alarm fields are required.",
"function-types": "Function types",
- "function-types-required": "Function types are required."
+ "function-types-required": "Function types are required.",
+ "maximum-function-types": "Maximum { count, select, 1 {1 function type is allowed.} other {# function types are allowed} }"
},
"datasource": {
"type": "Datasource type",
@@ -691,7 +693,13 @@ export default angular.module('thingsboard.locale', [])
"type-alarm": "Alarm",
"type-alarms": "Alarms",
"list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
- "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'"
+ "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
+ "search": "Search entities",
+ "selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
+ "entity-name": "Entity name",
+ "details": "Entity details",
+ "no-entities-prompt": "No entities found",
+ "no-data": "No data to display"
},
"event": {
"event-type": "Event type",
@@ -1144,6 +1152,7 @@ export default angular.module('thingsboard.locale', [])
"use-dashboard-timewindow": "Use dashboard timewindow",
"display-legend": "Display legend",
"datasources": "Datasources",
+ "maximum-datasources": "Maximum { count, select, 1 {1 datasource is allowed.} other {# datasources are allowed} }",
"datasource-type": "Type",
"datasource-parameters": "Parameters",
"remove-datasource": "Remove datasource",
ui/src/app/widget/lib/entities-table-widget.js 474(+474 -0)
diff --git a/ui/src/app/widget/lib/entities-table-widget.js b/ui/src/app/widget/lib/entities-table-widget.js
new file mode 100644
index 0000000..4b8aa6e
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-table-widget.js
@@ -0,0 +1,474 @@
+/*
+ * 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 './entities-table-widget.scss';
+
+/* eslint-disable import/no-unresolved, import/default */
+
+import entitiesTableWidgetTemplate from './entities-table-widget.tpl.html';
+//import entityDetailsDialogTemplate from './entitiy-details-dialog.tpl.html';
+
+/* eslint-enable import/no-unresolved, import/default */
+
+import tinycolor from 'tinycolor2';
+import cssjs from '../../../vendor/css.js/css';
+
+export default angular.module('thingsboard.widgets.entitiesTableWidget', [])
+ .directive('tbEntitiesTableWidget', EntitiesTableWidget)
+ .name;
+
+/*@ngInject*/
+function EntitiesTableWidget() {
+ return {
+ restrict: "E",
+ scope: true,
+ bindToController: {
+ tableId: '=',
+ ctx: '='
+ },
+ controller: EntitiesTableWidgetController,
+ controllerAs: 'vm',
+ templateUrl: entitiesTableWidgetTemplate
+ };
+}
+
+/*@ngInject*/
+function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $translate, utils, types) {
+ var vm = this;
+
+ vm.stylesInfo = {};
+ vm.contentsInfo = {};
+ vm.columnWidth = {};
+
+ vm.showData = true;
+ vm.hasData = false;
+
+ vm.entities = [];
+ vm.entitiesCount = 0;
+
+ vm.datasources = null;
+ vm.allEntities = null;
+
+ vm.currentEntity = null;
+
+ vm.displayEntityName = true;
+ vm.displayEntityType = true;
+ vm.displayActions = false; //TODO: Widget actions
+ vm.displayPagination = true;
+ vm.defaultPageSize = 10;
+ vm.defaultSortOrder = 'entityName';
+
+ vm.query = {
+ order: vm.defaultSortOrder,
+ limit: vm.defaultPageSize,
+ page: 1,
+ search: null
+ };
+
+ vm.searchAction = {
+ name: 'action.search',
+ show: true,
+ onAction: function() {
+ vm.enterFilterMode();
+ },
+ icon: 'search'
+ };
+
+ vm.enterFilterMode = enterFilterMode;
+ vm.exitFilterMode = exitFilterMode;
+ vm.onReorder = onReorder;
+ vm.onPaginate = onPaginate;
+ vm.onRowClick = onRowClick;
+ vm.isCurrent = isCurrent;
+
+ vm.cellStyle = cellStyle;
+ vm.cellContent = cellContent;
+
+ $scope.$watch('vm.ctx', function() {
+ if (vm.ctx) {
+ vm.settings = vm.ctx.settings;
+ vm.widgetConfig = vm.ctx.widgetConfig;
+ vm.subscription = vm.ctx.defaultSubscription;
+ vm.datasources = vm.subscription.datasources;
+ initializeConfig();
+ updateDatasources();
+ }
+ });
+
+ $scope.$watch("vm.query.search", function(newVal, prevVal) {
+ if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
+ updateEntities();
+ }
+ });
+
+ $scope.$on('entities-table-data-updated', function(event, tableId) {
+ if (vm.tableId == tableId) {
+ if (vm.subscription) {
+ updateEntitiesData(vm.subscription.data);
+ updateEntities();
+ $scope.$digest();
+ }
+ }
+ });
+
+ $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) {
+ vm.isGtXs = isGtXs;
+ });
+
+ $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) {
+ vm.isGtMd = isGtMd;
+ if (vm.isGtMd) {
+ vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3];
+ } else {
+ vm.limitOptions = null;
+ }
+ });
+
+ function initializeConfig() {
+
+ vm.ctx.widgetActions = [ vm.searchAction ];
+
+ if (vm.settings.entitiesTitle && vm.settings.entitiesTitle.length) {
+ var translationId = types.translate.customTranslationsPrefix + vm.settings.entitiesTitle;
+ var translation = $translate.instant(translationId);
+ if (translation != translationId) {
+ vm.entitiesTitle = translation + '';
+ } else {
+ vm.entitiesTitle = vm.settings.entitiesTitle;
+ }
+ } else {
+ vm.entitiesTitle = $translate.instant('entity.entities');
+ }
+
+ vm.ctx.widgetTitle = vm.entitiesTitle;
+
+ vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true;
+ vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true;
+ vm.displayEntityType = angular.isDefined(vm.settings.displayEntityType) ? vm.settings.displayEntityType : true;
+ vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true;
+
+ var pageSize = vm.settings.defaultPageSize;
+ if (angular.isDefined(pageSize) && Number.isInteger(pageSize) && pageSize > 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<vm.allEntities.length;i++) {
+ var entity = vm.allEntities[i];
+ for (var a=0;a<vm.dataKeys.length;a++) {
+ var dataKey = vm.dataKeys[a];
+ var index = i * vm.dataKeys.length + a;
+ var keyData = data[index].data;
+ if (keyData && keyData.length && keyData[0].length > 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<vm.datasources.length;i++) {
+ datasource = vm.datasources[i];
+ var entity = {
+ id: {}
+ };
+ entity.entityName = datasource.entityName;
+ if (datasource.entityId) {
+ entity.id.id = datasource.entityId;
+ }
+ if (datasource.entityType) {
+ entity.id.entityType = datasource.entityType;
+ entity.entityType = $translate.instant(types.entityTypeTranslations[datasource.entityType].type) + '';
+ } else {
+ entity.entityType = '';
+ }
+ for (d = 0; d < vm.dataKeys.length; d++) {
+ dataKey = vm.dataKeys[d];
+ entity[dataKey.name] = '';
+ }
+ vm.allEntities.push(entity);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/widget/lib/entities-table-widget.scss b/ui/src/app/widget/lib/entities-table-widget.scss
new file mode 100644
index 0000000..30c42cc
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-table-widget.scss
@@ -0,0 +1,58 @@
+/**
+ * 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-has-timewindow {
+ .tb-entities-table {
+ md-toolbar {
+ min-height: 60px;
+ max-height: 60px;
+ &.md-table-toolbar {
+ .md-toolbar-tools {
+ max-height: 60px;
+ }
+ }
+ }
+ }
+}
+
+.tb-entities-table {
+
+ md-toolbar {
+ min-height: 39px;
+ max-height: 39px;
+ &.md-table-toolbar {
+ .md-toolbar-tools {
+ max-height: 39px;
+ }
+ }
+ }
+
+ &.tb-data-table {
+ table.md-table, table.md-table.md-row-select {
+ tbody {
+ tr {
+ td {
+ &.tb-action-cell {
+ min-width: 36px;
+ max-width: 36px;
+ width: 36px;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ui/src/app/widget/lib/entities-table-widget.tpl.html b/ui/src/app/widget/lib/entities-table-widget.tpl.html
new file mode 100644
index 0000000..247d479
--- /dev/null
+++ b/ui/src/app/widget/lib/entities-table-widget.tpl.html
@@ -0,0 +1,86 @@
+<!--
+
+ 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.
+
+-->
+<div class="tb-absolute-fill tb-entities-table tb-data-table" layout="column">
+ <div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column">
+ <md-toolbar class="md-table-toolbar md-default" ng-show="vm.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">
+ {{'entity.search' | translate}}
+ </md-tooltip>
+ </md-button>
+ <md-input-container flex>
+ <label> </label>
+ <input ng-model="vm.query.search" placeholder="{{'entity.search' | translate}}"/>
+ </md-input-container>
+ <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
+ <md-icon aria-label="Close" class="material-icons">close</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'action.close' | translate }}
+ </md-tooltip>
+ </md-button>
+ </div>
+ </md-toolbar>
+ <md-table-container flex>
+ <table md-table>
+ <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
+ <tr md-row>
+ <th md-column ng-if="vm.displayEntityName" md-order-by="entityName"><span translate>entity.entity-name</span></th>
+ <th md-column ng-if="vm.displayEntityType" md-order-by="entityType"><span translate>entity.entity-type</span></th>
+ <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.dataKeys"><span>{{ key.title }}</span></th>
+ <th md-column ng-if="vm.displayActions"><span> </span></th>
+ </tr>
+ </thead>
+ <tbody md-body>
+ <tr ng-show="vm.entities.length" md-row md-select="entity"
+ md-select-id="id.id" md-auto-select="false" ng-repeat="entity in vm.entities"
+ ng-click="vm.onRowClick($event, entity)" ng-class="{'tb-current': vm.isCurrent(entity)}">
+ <td md-cell flex ng-if="vm.displayEntityName">{{entity.entityName}}</td>
+ <td md-cell flex ng-if="vm.displayEntityType">{{entity.entityType}}</td>
+ <td md-cell flex ng-repeat="key in vm.dataKeys"
+ ng-style="vm.cellStyle(entity, key)"
+ ng-bind-html="vm.cellContent(entity, key)">
+ </td>
+ <td md-cell ng-if="vm.displayActions" class="tb-action-cell">
+ <!--md-button class="md-icon-button" aria-label="{{ 'entity.details' | translate }}"
+ ng-click="vm.openEntityDetails($event, entity)">
+ <md-icon aria-label="{{ 'entity.details' | translate }}" class="material-icons">more_horiz</md-icon>
+ <md-tooltip md-direction="top">
+ {{ 'entity.details' | translate }}
+ </md-tooltip>
+ </md-button-->
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <md-divider></md-divider>
+ <span ng-show="!vm.entities.length"
+ layout-align="center center"
+ class="no-data-found" translate>entity.no-entities-prompt</span>
+ </md-table-container>
+ <md-table-pagination ng-if="vm.displayPagination" md-boundary-links md-limit="vm.query.limit" md-limit-options="vm.limitOptions"
+ md-page="vm.query.page" md-total="{{vm.entitiesCount}}"
+ md-on-paginate="vm.onPaginate" md-page-select="vm.isGtMd">
+ </md-table-pagination>
+ </div>
+ <span ng-show="!vm.showData"
+ layout-align="center center"
+ style="text-transform: uppercase; display: flex;"
+ class="tb-absolute-fill" translate>entity.no-data</span>
+</div>
diff --git a/ui/src/app/widget/widget-library.controller.js b/ui/src/app/widget/widget-library.controller.js
index bda8835..798227f 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 bd3e0c3..b2dd4a7 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</span>
</section>
<tb-dashboard
+ alias-controller="vm.aliasController"
widgets="vm.widgetTypes"
is-edit="false"
is-edit-action-enabled="true"