Commit 4e8c05f6707dd93d6b8c7e3da4772319f14d44ca

Authored by Igor Kulikov
Committed by GitHub
2 parents 3ecf91c5 d69e98e8

Merge pull request #175 from thingsboard/feature/TB-62

TB-62: Implement Entities table widget
... ... @@ -20,6 +20,7 @@ import tinycolor from 'tinycolor2';
20 20 import thingsboardLedLight from '../components/led-light.directive';
21 21 import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
22 22 import thingsboardAlarmsTableWidget from '../widget/lib/alarms-table-widget';
  23 +import thingsboardEntitiesTableWidget from '../widget/lib/entities-table-widget';
23 24
24 25 import TbFlot from '../widget/lib/flot-widget';
25 26 import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
... ... @@ -34,7 +35,7 @@ import thingsboardTypes from '../common/types.constant';
34 35 import thingsboardUtils from '../common/utils.service';
35 36
36 37 export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
37   - thingsboardAlarmsTableWidget, thingsboardTypes, thingsboardUtils])
  38 + thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardTypes, thingsboardUtils])
38 39 .factory('widgetService', WidgetService)
39 40 .name;
40 41
... ... @@ -546,6 +547,14 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
546 547
547 548 ' }\n\n' +
548 549
  550 + ' self.typeParameters = function() {\n\n' +
  551 + {
  552 + useCustomDatasources: false,
  553 + maxDatasources: -1 //unlimited
  554 + maxDataKeys: -1 //unlimited
  555 + }
  556 + ' }\n\n' +
  557 +
549 558 ' self.onResize = function() {\n\n' +
550 559
551 560 ' }\n\n' +
... ... @@ -586,10 +595,21 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
586 595 if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
587 596 result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
588 597 }
  598 + if (angular.isFunction(widgetTypeInstance.typeParameters)) {
  599 + result.typeParameters = widgetTypeInstance.typeParameters();
  600 + } else {
  601 + result.typeParameters = {};
  602 + }
589 603 if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) {
590   - result.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
  604 + result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
591 605 } else {
592   - result.useCustomDatasources = false;
  606 + result.typeParameters.useCustomDatasources = false;
  607 + }
  608 + if (angular.isUndefined(result.typeParameters.maxDatasources)) {
  609 + result.typeParameters.maxDatasources = -1;
  610 + }
  611 + if (angular.isUndefined(result.typeParameters.maxDataKeys)) {
  612 + result.typeParameters.maxDataKeys = -1;
593 613 }
594 614 return result;
595 615 } catch (e) {
... ... @@ -629,7 +649,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
629 649 if (widgetType.dataKeySettingsSchema) {
630 650 widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
631 651 }
632   - widgetInfo.useCustomDatasources = widgetType.useCustomDatasources;
  652 + widgetInfo.typeParameters = widgetType.typeParameters;
633 653 putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
634 654 putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
635 655 deferred.resolve(widgetInfo);
... ...
... ... @@ -156,11 +156,19 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
156 156 };
157 157
158 158 scope.transformTimeseriesDataKeyChip = function (chip) {
159   - return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries});
  159 + if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) {
  160 + return null;
  161 + } else {
  162 + return scope.generateDataKey({chip: chip, type: types.dataKeyType.timeseries});
  163 + }
160 164 };
161 165
162 166 scope.transformAttributeDataKeyChip = function (chip) {
163   - return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
  167 + if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) {
  168 + return null;
  169 + } else {
  170 + return scope.generateDataKey({chip: chip, type: types.dataKeyType.attribute});
  171 + }
164 172 };
165 173
166 174 scope.transformAlarmDataKeyChip = function (chip) {
... ... @@ -272,6 +280,7 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
272 280 require: "^ngModel",
273 281 scope: {
274 282 widgetType: '=',
  283 + maxDataKeys: '=',
275 284 aliasController: '=',
276 285 datakeySettingsSchema: '=',
277 286 generateDataKey: '&',
... ...
... ... @@ -186,5 +186,10 @@
186 186 <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.latest.value" class="tb-error-message">datakey.timeseries-or-attributes-required</div>
187 187 <div translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
188 188 </div>
  189 + <div class="md-caption" style="color: rgba(0,0,0,0.57);" ng-if="maxDataKeys != -1"
  190 + translate="datakey.maximum-timeseries-or-attributes"
  191 + translate-values="{count: maxDataKeys}"
  192 + translate-interpolation="messageformat"
  193 + ></div>
189 194 </section>
190 195 </section>
... ...
... ... @@ -117,7 +117,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
117 117 };
118 118
119 119 scope.transformFuncDataKeyChip = function (chip) {
120   - return scope.generateDataKey({chip: chip, type: types.dataKeyType.function});
  120 + if (scope.maxDataKeys > 0 && ngModelCtrl.$viewValue.dataKeys.length >= scope.maxDataKeys ) {
  121 + return null;
  122 + } else {
  123 + return scope.generateDataKey({chip: chip, type: types.dataKeyType.function});
  124 + }
121 125 };
122 126
123 127 scope.transformAlarmDataKeyChip = function (chip) {
... ... @@ -217,6 +221,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
217 221 require: "^ngModel",
218 222 scope: {
219 223 widgetType: '=',
  224 + maxDataKeys: '=',
220 225 generateDataKey: '&',
221 226 datakeySettingsSchema: '='
222 227 },
... ...
... ... @@ -17,7 +17,7 @@
17 17 -->
18 18 <section class="tb-datasource-func" flex layout='column'
19 19 layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center">
20   - <md-input-container ng-if="widgetType != types.widgetType.alarm.value"
  20 + <md-input-container ng-show="widgetType != types.widgetType.alarm.value"
21 21 class="tb-datasource-name" md-no-float style="min-width: 200px;">
22 22 <input name="datasourceName"
23 23 placeholder="{{ 'datasource.name' | translate }}"
... ... @@ -132,5 +132,10 @@
132 132 <div translate ng-message="datasourceKeys" ng-if="widgetType !== types.widgetType.alarm.value" class="tb-error-message">datakey.function-types-required</div>
133 133 <div translate ng-message="datasourceKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
134 134 </div>
  135 + <div class="md-caption" style="color: rgba(0,0,0,0.57);" ng-if="maxDataKeys != -1"
  136 + translate="datakey.maximum-function-types"
  137 + translate-values="{count: maxDataKeys}"
  138 + translate-interpolation="messageformat"
  139 + ></div>
135 140 </section>
136 141 </section>
... ...
... ... @@ -82,6 +82,7 @@ function Datasource($compile, $templateCache, utils, types) {
82 82 require: "^ngModel",
83 83 scope: {
84 84 aliasController: '=',
  85 + maxDataKeys: '=',
85 86 widgetType: '=',
86 87 functionsOnly: '=',
87 88 datakeySettingsSchema: '=',
... ...
... ... @@ -27,6 +27,7 @@
27 27 <tb-datasource-func flex
28 28 ng-switch-default
29 29 ng-model="model"
  30 + max-data-keys="maxDataKeys"
30 31 datakey-settings-schema="datakeySettingsSchema"
31 32 ng-required="model.type === types.datasourceType.function"
32 33 widget-type="widgetType"
... ... @@ -34,6 +35,7 @@
34 35 </tb-datasource-func>
35 36 <tb-datasource-entity flex
36 37 ng-model="model"
  38 + max-data-keys="maxDataKeys"
37 39 datakey-settings-schema="datakeySettingsSchema"
38 40 ng-switch-when="entity"
39 41 ng-required="model.type === types.datasourceType.entity"
... ...
... ... @@ -442,6 +442,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
442 442 forceExpandDatasources: '=?',
443 443 isDataEnabled: '=?',
444 444 widgetType: '=',
  445 + typeParameters: '=',
445 446 widgetSettingsSchema: '=',
446 447 datakeySettingsSchema: '=',
447 448 aliasController: '=',
... ...
... ... @@ -62,7 +62,14 @@
62 62 && isDataEnabled">
63 63 <v-pane id="datasources-pane" expanded="true">
64 64 <v-pane-header>
65   - {{ 'widget-config.datasources' | translate }}
  65 + <div layout="column">
  66 + <div>{{ 'widget-config.datasources' | translate }}</div>
  67 + <div class="md-caption" style="color: rgba(0,0,0,0.57);" ng-if="typeParameters.maxDatasources != -1"
  68 + translate="widget-config.maximum-datasources"
  69 + translate-values="{count: typeParameters.maxDatasources}"
  70 + translate-interpolation="messageformat"
  71 + ></div>
  72 + </div>
66 73 </v-pane-header>
67 74 <v-pane-content>
68 75 <div ng-if="datasources.length === 0">
... ... @@ -88,6 +95,7 @@
88 95 style="padding: 0 0 0 10px; margin: 5px;">
89 96 <tb-datasource flex ng-model="datasource.value"
90 97 widget-type="widgetType"
  98 + max-data-keys="typeParameters.maxDataKeys"
91 99 alias-controller="aliasController"
92 100 functions-only="functionsOnly"
93 101 datakey-settings-schema="datakeySettingsSchema"
... ... @@ -111,7 +119,7 @@
111 119 </div>
112 120 </div>
113 121 <div flex layout="row" layout-align="start center">
114   - <md-button ng-disabled="loading" class="md-primary md-raised"
  122 + <md-button ng-show="typeParameters.maxDatasources == -1 || datasources.length < typeParameters.maxDatasources" ng-disabled="loading" class="md-primary md-raised"
115 123 ng-click="addDatasource($event)" aria-label="{{ 'action.add' | translate }}">
116 124 <md-tooltip md-direction="top">
117 125 {{ 'widget-config.add-datasource' | translate }}
... ... @@ -140,7 +148,7 @@
140 148 </v-pane-content>
141 149 </v-pane>
142 150 </v-accordion>
143   - <v-accordion id="alarn-source-accordion" control="alarmSourceAccordion" class="vAccordion--default"
  151 + <v-accordion id="alarm-source-accordion" control="alarmSourceAccordion" class="vAccordion--default"
144 152 ng-if="widgetType === types.widgetType.alarm.value && isDataEnabled">
145 153 <v-pane id="alarm-source-pane" expanded="true">
146 154 <v-pane-header>
... ...
... ... @@ -128,7 +128,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
128 128
129 129 var widgetTypeInstance;
130 130
131   - vm.useCustomDatasources = false;
  131 + vm.typeParameters = widgetInfo.typeParameters;
132 132
133 133 try {
134 134 widgetTypeInstance = new widgetType(widgetContext);
... ... @@ -154,9 +154,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
154 154 if (!widgetTypeInstance.onDestroy) {
155 155 widgetTypeInstance.onDestroy = function() {};
156 156 }
157   - if (widgetTypeInstance.useCustomDatasources) {
158   - vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
159   - }
160 157
161 158 //TODO: widgets visibility
162 159
... ... @@ -502,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
502 499 var subscription = widgetContext.subscriptions[id];
503 500 subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds);
504 501 }
505   - if (subscriptionChanged && !vm.useCustomDatasources) {
  502 + if (subscriptionChanged && !vm.typeParameters.useCustomDatasources) {
506 503 reInit();
507 504 }
508 505 });
... ... @@ -513,7 +510,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
513 510
514 511 configureWidgetElement();
515 512 var deferred = $q.defer();
516   - if (!vm.useCustomDatasources) {
  513 + if (!vm.typeParameters.useCustomDatasources) {
517 514 createDefaultSubscription().then(
518 515 function success() {
519 516 subscriptionInited = true;
... ... @@ -535,7 +532,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
535 532 function reInit() {
536 533 onDestroy();
537 534 configureWidgetElement();
538   - if (!vm.useCustomDatasources) {
  535 + if (!vm.typeParameters.useCustomDatasources) {
539 536 createDefaultSubscription().then(
540 537 function success() {
541 538 subscriptionInited = true;
... ... @@ -575,7 +572,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
575 572 } catch (e) {
576 573 handleWidgetException(e);
577 574 }
578   - if (!vm.useCustomDatasources && widgetContext.defaultSubscription) {
  575 + if (!vm.typeParameters.useCustomDatasources && widgetContext.defaultSubscription) {
579 576 widgetContext.defaultSubscription.subscribe();
580 577 }
581 578 }
... ...
... ... @@ -33,6 +33,7 @@
33 33 <div class="md-dialog-content" style="padding-top: 0px;">
34 34 <fieldset ng-disabled="loading" style="position: relative; height: 600px;">
35 35 <tb-widget-config widget-type="vm.widget.type"
  36 + type-parameters="vm.widgetInfo.typeParameters"
36 37 force-expand-datasources="true"
37 38 ng-model="vm.widgetConfig"
38 39 widget-settings-schema="vm.settingsSchema"
... ...
... ... @@ -918,7 +918,7 @@ export default function DashboardController(types, utils, dashboardUtils, widget
918 918 }
919 919 }
920 920
921   - if (widgetTypeInfo.useCustomDatasources) {
  921 + if (widgetTypeInfo.typeParameters.useCustomDatasources) {
922 922 addWidget(newWidget);
923 923 } else {
924 924 $mdDialog.show({
... ...
... ... @@ -212,6 +212,7 @@
212 212 class="tb-absolute-fill" md-border-bottom>
213 213 <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
214 214 <tb-dashboard
  215 + alias-controller="vm.dashboardCtx.aliasController"
215 216 widgets="vm.timeseriesWidgetTypes"
216 217 is-edit="false"
217 218 is-mobile="true"
... ... @@ -222,6 +223,7 @@
222 223 </md-tab>
223 224 <md-tab ng-if="vm.latestWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">
224 225 <tb-dashboard
  226 + alias-controller="vm.dashboardCtx.aliasController"
225 227 widgets="vm.latestWidgetTypes"
226 228 is-edit="false"
227 229 is-mobile="true"
... ... @@ -232,6 +234,7 @@
232 234 </md-tab>
233 235 <md-tab ng-if="vm.rpcWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">
234 236 <tb-dashboard
  237 + alias-controller="vm.dashboardCtx.aliasController"
235 238 widgets="vm.rpcWidgetTypes"
236 239 is-edit="false"
237 240 is-mobile="true"
... ... @@ -242,6 +245,7 @@
242 245 </md-tab>
243 246 <md-tab ng-if="vm.alarmWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">
244 247 <tb-dashboard
  248 + alias-controller="vm.dashboardCtx.aliasController"
245 249 widgets="vm.alarmWidgetTypes"
246 250 is-edit="false"
247 251 is-mobile="true"
... ... @@ -252,6 +256,7 @@
252 256 </md-tab>
253 257 <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
254 258 <tb-dashboard
  259 + alias-controller="vm.dashboardCtx.aliasController"
255 260 widgets="vm.staticWidgetTypes"
256 261 is-edit="false"
257 262 is-mobile="true"
... ...
... ... @@ -40,7 +40,8 @@ export default function EditWidgetDirective($compile, $templateCache, types, wid
40 40 };
41 41 var settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
42 42 var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
43   - scope.isDataEnabled = !widgetInfo.useCustomDatasources;
  43 + scope.typeParameters = widgetInfo.typeParameters;
  44 + scope.isDataEnabled = !widgetInfo.typeParameters.useCustomDatasources;
44 45 if (!settingsSchema || settingsSchema === '') {
45 46 scope.settingsSchema = {};
46 47 } else {
... ...
... ... @@ -17,6 +17,7 @@
17 17 -->
18 18 <fieldset ng-disabled="loading">
19 19 <tb-widget-config widget-type="widget.type"
  20 + type-parameters="typeParameters"
20 21 ng-model="widgetConfig"
21 22 is-data-enabled="isDataEnabled"
22 23 widget-settings-schema="settingsSchema"
... ...
... ... @@ -499,9 +499,11 @@ export default angular.module('thingsboard.locale', [])
499 499 "alarm": "Alarm fields",
500 500 "timeseries-required": "Entity timeseries are required.",
501 501 "timeseries-or-attributes-required": "Entity timeseries/attributes are required.",
  502 + "maximum-timeseries-or-attributes": "Maximum { count, select, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }",
502 503 "alarm-fields-required": "Alarm fields are required.",
503 504 "function-types": "Function types",
504   - "function-types-required": "Function types are required."
  505 + "function-types-required": "Function types are required.",
  506 + "maximum-function-types": "Maximum { count, select, 1 {1 function type is allowed.} other {# function types are allowed} }"
505 507 },
506 508 "datasource": {
507 509 "type": "Datasource type",
... ... @@ -691,7 +693,13 @@ export default angular.module('thingsboard.locale', [])
691 693 "type-alarm": "Alarm",
692 694 "type-alarms": "Alarms",
693 695 "list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
694   - "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'"
  696 + "alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
  697 + "search": "Search entities",
  698 + "selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
  699 + "entity-name": "Entity name",
  700 + "details": "Entity details",
  701 + "no-entities-prompt": "No entities found",
  702 + "no-data": "No data to display"
695 703 },
696 704 "event": {
697 705 "event-type": "Event type",
... ... @@ -1144,6 +1152,7 @@ export default angular.module('thingsboard.locale', [])
1144 1152 "use-dashboard-timewindow": "Use dashboard timewindow",
1145 1153 "display-legend": "Display legend",
1146 1154 "datasources": "Datasources",
  1155 + "maximum-datasources": "Maximum { count, select, 1 {1 datasource is allowed.} other {# datasources are allowed} }",
1147 1156 "datasource-type": "Type",
1148 1157 "datasource-parameters": "Parameters",
1149 1158 "remove-datasource": "Remove datasource",
... ...
  1 +/*
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +import './entities-table-widget.scss';
  18 +
  19 +/* eslint-disable import/no-unresolved, import/default */
  20 +
  21 +import entitiesTableWidgetTemplate from './entities-table-widget.tpl.html';
  22 +//import entityDetailsDialogTemplate from './entitiy-details-dialog.tpl.html';
  23 +
  24 +/* eslint-enable import/no-unresolved, import/default */
  25 +
  26 +import tinycolor from 'tinycolor2';
  27 +import cssjs from '../../../vendor/css.js/css';
  28 +
  29 +export default angular.module('thingsboard.widgets.entitiesTableWidget', [])
  30 + .directive('tbEntitiesTableWidget', EntitiesTableWidget)
  31 + .name;
  32 +
  33 +/*@ngInject*/
  34 +function EntitiesTableWidget() {
  35 + return {
  36 + restrict: "E",
  37 + scope: true,
  38 + bindToController: {
  39 + tableId: '=',
  40 + ctx: '='
  41 + },
  42 + controller: EntitiesTableWidgetController,
  43 + controllerAs: 'vm',
  44 + templateUrl: entitiesTableWidgetTemplate
  45 + };
  46 +}
  47 +
  48 +/*@ngInject*/
  49 +function EntitiesTableWidgetController($element, $scope, $filter, $mdMedia, $translate, utils, types) {
  50 + var vm = this;
  51 +
  52 + vm.stylesInfo = {};
  53 + vm.contentsInfo = {};
  54 + vm.columnWidth = {};
  55 +
  56 + vm.showData = true;
  57 + vm.hasData = false;
  58 +
  59 + vm.entities = [];
  60 + vm.entitiesCount = 0;
  61 +
  62 + vm.datasources = null;
  63 + vm.allEntities = null;
  64 +
  65 + vm.currentEntity = null;
  66 +
  67 + vm.displayEntityName = true;
  68 + vm.displayEntityType = true;
  69 + vm.displayActions = false; //TODO: Widget actions
  70 + vm.displayPagination = true;
  71 + vm.defaultPageSize = 10;
  72 + vm.defaultSortOrder = 'entityName';
  73 +
  74 + vm.query = {
  75 + order: vm.defaultSortOrder,
  76 + limit: vm.defaultPageSize,
  77 + page: 1,
  78 + search: null
  79 + };
  80 +
  81 + vm.searchAction = {
  82 + name: 'action.search',
  83 + show: true,
  84 + onAction: function() {
  85 + vm.enterFilterMode();
  86 + },
  87 + icon: 'search'
  88 + };
  89 +
  90 + vm.enterFilterMode = enterFilterMode;
  91 + vm.exitFilterMode = exitFilterMode;
  92 + vm.onReorder = onReorder;
  93 + vm.onPaginate = onPaginate;
  94 + vm.onRowClick = onRowClick;
  95 + vm.isCurrent = isCurrent;
  96 +
  97 + vm.cellStyle = cellStyle;
  98 + vm.cellContent = cellContent;
  99 +
  100 + $scope.$watch('vm.ctx', function() {
  101 + if (vm.ctx) {
  102 + vm.settings = vm.ctx.settings;
  103 + vm.widgetConfig = vm.ctx.widgetConfig;
  104 + vm.subscription = vm.ctx.defaultSubscription;
  105 + vm.datasources = vm.subscription.datasources;
  106 + initializeConfig();
  107 + updateDatasources();
  108 + }
  109 + });
  110 +
  111 + $scope.$watch("vm.query.search", function(newVal, prevVal) {
  112 + if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
  113 + updateEntities();
  114 + }
  115 + });
  116 +
  117 + $scope.$on('entities-table-data-updated', function(event, tableId) {
  118 + if (vm.tableId == tableId) {
  119 + if (vm.subscription) {
  120 + updateEntitiesData(vm.subscription.data);
  121 + updateEntities();
  122 + $scope.$digest();
  123 + }
  124 + }
  125 + });
  126 +
  127 + $scope.$watch(function() { return $mdMedia('gt-xs'); }, function(isGtXs) {
  128 + vm.isGtXs = isGtXs;
  129 + });
  130 +
  131 + $scope.$watch(function() { return $mdMedia('gt-md'); }, function(isGtMd) {
  132 + vm.isGtMd = isGtMd;
  133 + if (vm.isGtMd) {
  134 + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3];
  135 + } else {
  136 + vm.limitOptions = null;
  137 + }
  138 + });
  139 +
  140 + function initializeConfig() {
  141 +
  142 + vm.ctx.widgetActions = [ vm.searchAction ];
  143 +
  144 + if (vm.settings.entitiesTitle && vm.settings.entitiesTitle.length) {
  145 + var translationId = types.translate.customTranslationsPrefix + vm.settings.entitiesTitle;
  146 + var translation = $translate.instant(translationId);
  147 + if (translation != translationId) {
  148 + vm.entitiesTitle = translation + '';
  149 + } else {
  150 + vm.entitiesTitle = vm.settings.entitiesTitle;
  151 + }
  152 + } else {
  153 + vm.entitiesTitle = $translate.instant('entity.entities');
  154 + }
  155 +
  156 + vm.ctx.widgetTitle = vm.entitiesTitle;
  157 +
  158 + vm.searchAction.show = angular.isDefined(vm.settings.enableSearch) ? vm.settings.enableSearch : true;
  159 + vm.displayEntityName = angular.isDefined(vm.settings.displayEntityName) ? vm.settings.displayEntityName : true;
  160 + vm.displayEntityType = angular.isDefined(vm.settings.displayEntityType) ? vm.settings.displayEntityType : true;
  161 + vm.displayPagination = angular.isDefined(vm.settings.displayPagination) ? vm.settings.displayPagination : true;
  162 +
  163 + var pageSize = vm.settings.defaultPageSize;
  164 + if (angular.isDefined(pageSize) && Number.isInteger(pageSize) && pageSize > 0) {
  165 + vm.defaultPageSize = pageSize;
  166 + }
  167 +
  168 + if (vm.settings.defaultSortOrder && vm.settings.defaultSortOrder.length) {
  169 + vm.defaultSortOrder = vm.settings.defaultSortOrder;
  170 + }
  171 +
  172 + vm.query.order = vm.defaultSortOrder;
  173 + vm.query.limit = vm.defaultPageSize;
  174 + if (vm.isGtMd) {
  175 + vm.limitOptions = [vm.defaultPageSize, vm.defaultPageSize*2, vm.defaultPageSize*3];
  176 + } else {
  177 + vm.limitOptions = null;
  178 + }
  179 +
  180 + var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
  181 + var defaultColor = tinycolor(origColor);
  182 + var mdDark = defaultColor.setAlpha(0.87).toRgbString();
  183 + var mdDarkSecondary = defaultColor.setAlpha(0.54).toRgbString();
  184 + var mdDarkDisabled = defaultColor.setAlpha(0.26).toRgbString();
  185 + //var mdDarkIcon = mdDarkSecondary;
  186 + var mdDarkDivider = defaultColor.setAlpha(0.12).toRgbString();
  187 +
  188 + var cssString = 'table.md-table th.md-column {\n'+
  189 + 'color: ' + mdDarkSecondary + ';\n'+
  190 + '}\n'+
  191 + 'table.md-table th.md-column.md-checkbox-column md-checkbox:not(.md-checked) .md-icon {\n'+
  192 + 'border-color: ' + mdDarkSecondary + ';\n'+
  193 + '}\n'+
  194 + 'table.md-table th.md-column md-icon.md-sort-icon {\n'+
  195 + 'color: ' + mdDarkDisabled + ';\n'+
  196 + '}\n'+
  197 + 'table.md-table th.md-column.md-active, table.md-table th.md-column.md-active md-icon {\n'+
  198 + 'color: ' + mdDark + ';\n'+
  199 + '}\n'+
  200 + 'table.md-table td.md-cell {\n'+
  201 + 'color: ' + mdDark + ';\n'+
  202 + 'border-top: 1px '+mdDarkDivider+' solid;\n'+
  203 + '}\n'+
  204 + 'table.md-table td.md-cell.md-checkbox-cell md-checkbox:not(.md-checked) .md-icon {\n'+
  205 + 'border-color: ' + mdDarkSecondary + ';\n'+
  206 + '}\n'+
  207 + 'table.md-table td.md-cell.md-placeholder {\n'+
  208 + 'color: ' + mdDarkDisabled + ';\n'+
  209 + '}\n'+
  210 + 'table.md-table td.md-cell md-select > .md-select-value > span.md-select-icon {\n'+
  211 + 'color: ' + mdDarkSecondary + ';\n'+
  212 + '}\n'+
  213 + '.md-table-pagination {\n'+
  214 + 'color: ' + mdDarkSecondary + ';\n'+
  215 + 'border-top: 1px '+mdDarkDivider+' solid;\n'+
  216 + '}\n'+
  217 + '.md-table-pagination .buttons md-icon {\n'+
  218 + 'color: ' + mdDarkSecondary + ';\n'+
  219 + '}\n'+
  220 + '.md-table-pagination md-select:not([disabled]):focus .md-select-value {\n'+
  221 + 'color: ' + mdDarkSecondary + ';\n'+
  222 + '}';
  223 +
  224 + var cssParser = new cssjs();
  225 + cssParser.testMode = false;
  226 + var namespace = 'entities-table-' + hashCode(cssString);
  227 + cssParser.cssPreviewNamespace = namespace;
  228 + cssParser.createStyleElement(namespace, cssString);
  229 + $element.addClass(namespace);
  230 +
  231 + function hashCode(str) {
  232 + var hash = 0;
  233 + var i, char;
  234 + if (str.length === 0) return hash;
  235 + for (i = 0; i < str.length; i++) {
  236 + char = str.charCodeAt(i);
  237 + hash = ((hash << 5) - hash) + char;
  238 + hash = hash & hash;
  239 + }
  240 + return hash;
  241 + }
  242 + }
  243 +
  244 + function enterFilterMode () {
  245 + vm.query.search = '';
  246 + vm.ctx.hideTitlePanel = true;
  247 + }
  248 +
  249 + function exitFilterMode () {
  250 + vm.query.search = null;
  251 + updateEntities();
  252 + vm.ctx.hideTitlePanel = false;
  253 + }
  254 +
  255 + function onReorder () {
  256 + updateEntities();
  257 + }
  258 +
  259 + function onPaginate () {
  260 + updateEntities();
  261 + }
  262 +
  263 + function onRowClick($event, entity) {
  264 + if (vm.currentEntity != entity) {
  265 + vm.currentEntity = entity;
  266 + }
  267 + }
  268 +
  269 + function isCurrent(entity) {
  270 + return (vm.currentEntity && entity && vm.currentEntity.id && entity.id) &&
  271 + (vm.currentEntity.id.id === entity.id.id);
  272 + }
  273 +
  274 + function updateEntities() {
  275 + var result = $filter('orderBy')(vm.allEntities, vm.query.order);
  276 + if (vm.query.search != null) {
  277 + result = $filter('filter')(result, {$: vm.query.search});
  278 + }
  279 + vm.entitiesCount = result.length;
  280 +
  281 + if (vm.displayPagination) {
  282 + var startIndex = vm.query.limit * (vm.query.page - 1);
  283 + vm.entities = result.slice(startIndex, startIndex + vm.query.limit);
  284 + } else {
  285 + vm.entities = result;
  286 + }
  287 + }
  288 +
  289 + function cellStyle(entity, key) {
  290 + var style = {};
  291 + if (entity && key) {
  292 + var styleInfo = vm.stylesInfo[key.label];
  293 + var value = getEntityValue(entity, key);
  294 + if (styleInfo.useCellStyleFunction && styleInfo.cellStyleFunction) {
  295 + try {
  296 + style = styleInfo.cellStyleFunction(value);
  297 + } catch (e) {
  298 + style = {};
  299 + }
  300 + } else {
  301 + style = defaultStyle(key, value);
  302 + }
  303 + }
  304 + if (!style.width) {
  305 + var columnWidth = vm.columnWidth[key.label];
  306 + style.width = columnWidth;
  307 + }
  308 + return style;
  309 + }
  310 +
  311 + function cellContent(entity, key) {
  312 + var strContent = '';
  313 + if (entity && key) {
  314 + var contentInfo = vm.contentsInfo[key.label];
  315 + var value = getEntityValue(entity, key);
  316 + if (contentInfo.useCellContentFunction && contentInfo.cellContentFunction) {
  317 + if (angular.isDefined(value)) {
  318 + strContent = '' + value;
  319 + }
  320 + var content = strContent;
  321 + try {
  322 + content = contentInfo.cellContentFunction(value, entity, $filter);
  323 + } catch (e) {
  324 + content = strContent;
  325 + }
  326 + } else {
  327 + content = defaultContent(key, value);
  328 + }
  329 + return content;
  330 + } else {
  331 + return strContent;
  332 + }
  333 + }
  334 +
  335 + function defaultContent(key, value) {
  336 + if (angular.isDefined(value)) {
  337 + return value;
  338 + } else {
  339 + return '';
  340 + }
  341 + }
  342 +
  343 + function defaultStyle(/*key, value*/) {
  344 + return {};
  345 + }
  346 +
  347 + const getDescendantProp = (obj, path) => (
  348 + path.split('.').reduce((acc, part) => acc && acc[part], obj)
  349 + );
  350 +
  351 + function getEntityValue(entity, key) {
  352 + return getDescendantProp(entity, key.name);
  353 + }
  354 +
  355 + function updateEntitiesData(data) {
  356 + if (vm.allEntities) {
  357 + for (var i=0;i<vm.allEntities.length;i++) {
  358 + var entity = vm.allEntities[i];
  359 + for (var a=0;a<vm.dataKeys.length;a++) {
  360 + var dataKey = vm.dataKeys[a];
  361 + var index = i * vm.dataKeys.length + a;
  362 + var keyData = data[index].data;
  363 + if (keyData && keyData.length && keyData[0].length > 1) {
  364 + var value = keyData[0][1];
  365 + entity[dataKey.name] = value;
  366 + } else {
  367 + entity[dataKey.name] = '';
  368 + }
  369 + }
  370 + }
  371 + }
  372 + }
  373 +
  374 + function updateDatasources() {
  375 +
  376 + vm.stylesInfo = {};
  377 + vm.contentsInfo = {};
  378 + vm.columnWidth = {};
  379 + vm.dataKeys = [];
  380 + vm.allEntities = [];
  381 +
  382 + var datasource;
  383 + var dataKey;
  384 +
  385 + datasource = vm.datasources[0];
  386 +
  387 + vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.entitiesTitle);
  388 +
  389 + for (var d = 0; d < datasource.dataKeys.length; d++ ) {
  390 + dataKey = angular.copy(datasource.dataKeys[d]);
  391 + if (dataKey.type == types.dataKeyType.function) {
  392 + dataKey.name = dataKey.label;
  393 + }
  394 + vm.dataKeys.push(dataKey);
  395 +
  396 + var translationId = types.translate.customTranslationsPrefix + dataKey.label;
  397 + var translation = $translate.instant(translationId);
  398 + if (translation != translationId) {
  399 + dataKey.title = translation + '';
  400 + } else {
  401 + dataKey.title = dataKey.label;
  402 + }
  403 +
  404 + var keySettings = dataKey.settings;
  405 +
  406 + var cellStyleFunction = null;
  407 + var useCellStyleFunction = false;
  408 +
  409 + if (keySettings.useCellStyleFunction === true) {
  410 + if (angular.isDefined(keySettings.cellStyleFunction) && keySettings.cellStyleFunction.length > 0) {
  411 + try {
  412 + cellStyleFunction = new Function('value', keySettings.cellStyleFunction);
  413 + useCellStyleFunction = true;
  414 + } catch (e) {
  415 + cellStyleFunction = null;
  416 + useCellStyleFunction = false;
  417 + }
  418 + }
  419 + }
  420 +
  421 + vm.stylesInfo[dataKey.label] = {
  422 + useCellStyleFunction: useCellStyleFunction,
  423 + cellStyleFunction: cellStyleFunction
  424 + };
  425 +
  426 + var cellContentFunction = null;
  427 + var useCellContentFunction = false;
  428 +
  429 + if (keySettings.useCellContentFunction === true) {
  430 + if (angular.isDefined(keySettings.cellContentFunction) && keySettings.cellContentFunction.length > 0) {
  431 + try {
  432 + cellContentFunction = new Function('value, entity, filter', keySettings.cellContentFunction);
  433 + useCellContentFunction = true;
  434 + } catch (e) {
  435 + cellContentFunction = null;
  436 + useCellContentFunction = false;
  437 + }
  438 + }
  439 + }
  440 +
  441 + vm.contentsInfo[dataKey.label] = {
  442 + useCellContentFunction: useCellContentFunction,
  443 + cellContentFunction: cellContentFunction
  444 + };
  445 +
  446 + var columnWidth = angular.isDefined(keySettings.columnWidth) ? keySettings.columnWidth : '0px';
  447 + vm.columnWidth[dataKey.label] = columnWidth;
  448 + }
  449 +
  450 + for (var i=0;i<vm.datasources.length;i++) {
  451 + datasource = vm.datasources[i];
  452 + var entity = {
  453 + id: {}
  454 + };
  455 + entity.entityName = datasource.entityName;
  456 + if (datasource.entityId) {
  457 + entity.id.id = datasource.entityId;
  458 + }
  459 + if (datasource.entityType) {
  460 + entity.id.entityType = datasource.entityType;
  461 + entity.entityType = $translate.instant(types.entityTypeTranslations[datasource.entityType].type) + '';
  462 + } else {
  463 + entity.entityType = '';
  464 + }
  465 + for (d = 0; d < vm.dataKeys.length; d++) {
  466 + dataKey = vm.dataKeys[d];
  467 + entity[dataKey.name] = '';
  468 + }
  469 + vm.allEntities.push(entity);
  470 + }
  471 +
  472 + }
  473 +
  474 +}
\ No newline at end of file
... ...
  1 +/**
  2 + * Copyright © 2016-2017 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-has-timewindow {
  18 + .tb-entities-table {
  19 + md-toolbar {
  20 + min-height: 60px;
  21 + max-height: 60px;
  22 + &.md-table-toolbar {
  23 + .md-toolbar-tools {
  24 + max-height: 60px;
  25 + }
  26 + }
  27 + }
  28 + }
  29 +}
  30 +
  31 +.tb-entities-table {
  32 +
  33 + md-toolbar {
  34 + min-height: 39px;
  35 + max-height: 39px;
  36 + &.md-table-toolbar {
  37 + .md-toolbar-tools {
  38 + max-height: 39px;
  39 + }
  40 + }
  41 + }
  42 +
  43 + &.tb-data-table {
  44 + table.md-table, table.md-table.md-row-select {
  45 + tbody {
  46 + tr {
  47 + td {
  48 + &.tb-action-cell {
  49 + min-width: 36px;
  50 + max-width: 36px;
  51 + width: 36px;
  52 + }
  53 + }
  54 + }
  55 + }
  56 + }
  57 + }
  58 +}
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2017 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div class="tb-absolute-fill tb-entities-table tb-data-table" layout="column">
  19 + <div ng-show="vm.showData" flex class="tb-absolute-fill" layout="column">
  20 + <md-toolbar class="md-table-toolbar md-default" ng-show="vm.query.search != null">
  21 + <div class="md-toolbar-tools">
  22 + <md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
  23 + <md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
  24 + <md-tooltip md-direction="top">
  25 + {{'entity.search' | translate}}
  26 + </md-tooltip>
  27 + </md-button>
  28 + <md-input-container flex>
  29 + <label>&nbsp;</label>
  30 + <input ng-model="vm.query.search" placeholder="{{'entity.search' | translate}}"/>
  31 + </md-input-container>
  32 + <md-button class="md-icon-button" aria-label="Close" ng-click="vm.exitFilterMode()">
  33 + <md-icon aria-label="Close" class="material-icons">close</md-icon>
  34 + <md-tooltip md-direction="top">
  35 + {{ 'action.close' | translate }}
  36 + </md-tooltip>
  37 + </md-button>
  38 + </div>
  39 + </md-toolbar>
  40 + <md-table-container flex>
  41 + <table md-table>
  42 + <thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
  43 + <tr md-row>
  44 + <th md-column ng-if="vm.displayEntityName" md-order-by="entityName"><span translate>entity.entity-name</span></th>
  45 + <th md-column ng-if="vm.displayEntityType" md-order-by="entityType"><span translate>entity.entity-type</span></th>
  46 + <th md-column md-order-by="{{ key.name }}" ng-repeat="key in vm.dataKeys"><span>{{ key.title }}</span></th>
  47 + <th md-column ng-if="vm.displayActions"><span>&nbsp</span></th>
  48 + </tr>
  49 + </thead>
  50 + <tbody md-body>
  51 + <tr ng-show="vm.entities.length" md-row md-select="entity"
  52 + md-select-id="id.id" md-auto-select="false" ng-repeat="entity in vm.entities"
  53 + ng-click="vm.onRowClick($event, entity)" ng-class="{'tb-current': vm.isCurrent(entity)}">
  54 + <td md-cell flex ng-if="vm.displayEntityName">{{entity.entityName}}</td>
  55 + <td md-cell flex ng-if="vm.displayEntityType">{{entity.entityType}}</td>
  56 + <td md-cell flex ng-repeat="key in vm.dataKeys"
  57 + ng-style="vm.cellStyle(entity, key)"
  58 + ng-bind-html="vm.cellContent(entity, key)">
  59 + </td>
  60 + <td md-cell ng-if="vm.displayActions" class="tb-action-cell">
  61 + <!--md-button class="md-icon-button" aria-label="{{ 'entity.details' | translate }}"
  62 + ng-click="vm.openEntityDetails($event, entity)">
  63 + <md-icon aria-label="{{ 'entity.details' | translate }}" class="material-icons">more_horiz</md-icon>
  64 + <md-tooltip md-direction="top">
  65 + {{ 'entity.details' | translate }}
  66 + </md-tooltip>
  67 + </md-button-->
  68 + </td>
  69 + </tr>
  70 + </tbody>
  71 + </table>
  72 + <md-divider></md-divider>
  73 + <span ng-show="!vm.entities.length"
  74 + layout-align="center center"
  75 + class="no-data-found" translate>entity.no-entities-prompt</span>
  76 + </md-table-container>
  77 + <md-table-pagination ng-if="vm.displayPagination" md-boundary-links md-limit="vm.query.limit" md-limit-options="vm.limitOptions"
  78 + md-page="vm.query.page" md-total="{{vm.entitiesCount}}"
  79 + md-on-paginate="vm.onPaginate" md-page-select="vm.isGtMd">
  80 + </md-table-pagination>
  81 + </div>
  82 + <span ng-show="!vm.showData"
  83 + layout-align="center center"
  84 + style="text-transform: uppercase; display: flex;"
  85 + class="tb-absolute-fill" translate>entity.no-data</span>
  86 +</div>
... ...
... ... @@ -13,6 +13,9 @@
13 13 * See the License for the specific language governing permissions and
14 14 * limitations under the License.
15 15 */
  16 +
  17 +import AliasController from '../api/alias-controller';
  18 +
16 19 /* eslint-disable import/no-unresolved, import/default */
17 20
18 21 import selectWidgetTypeTemplate from './select-widget-type.tpl.html';
... ... @@ -21,7 +24,8 @@ import selectWidgetTypeTemplate from './select-widget-type.tpl.html';
21 24
22 25 /*@ngInject*/
23 26 export default function WidgetLibraryController($scope, $rootScope, $q, widgetService, userService, importExport,
24   - $state, $stateParams, $document, $mdDialog, $translate, $filter, types) {
  27 + $state, $stateParams, $document, $mdDialog, $translate, $filter,
  28 + utils, types, entityService) {
25 29
26 30 var vm = this;
27 31
... ... @@ -31,6 +35,14 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
31 35 vm.widgetTypes = [];
32 36 vm.dashboardInitComplete = false;
33 37
  38 + var stateController = {
  39 + getStateParams: function() {
  40 + return {};
  41 + }
  42 + };
  43 + vm.aliasController = new AliasController($scope, $q, $filter, utils,
  44 + types, entityService, stateController, {});
  45 +
34 46 vm.noData = noData;
35 47 vm.dashboardInited = dashboardInited;
36 48 vm.dashboardInitFailed = dashboardInitFailed;
... ...
... ... @@ -28,6 +28,7 @@
28 28 class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
29 29 </section>
30 30 <tb-dashboard
  31 + alias-controller="vm.aliasController"
31 32 widgets="vm.widgetTypes"
32 33 is-edit="false"
33 34 is-edit-action-enabled="true"
... ...