Browse Source

TB-62: Implement Entities table widget

pull/175/head
Igor Kulikov 9 years ago
parent
commit
d69e98e8b9
  1. 28
      ui/src/app/api/widget.service.js
  2. 13
      ui/src/app/components/datasource-entity.directive.js
  3. 5
      ui/src/app/components/datasource-entity.tpl.html
  4. 7
      ui/src/app/components/datasource-func.directive.js
  5. 7
      ui/src/app/components/datasource-func.tpl.html
  6. 1
      ui/src/app/components/datasource.directive.js
  7. 2
      ui/src/app/components/datasource.tpl.html
  8. 1
      ui/src/app/components/widget-config.directive.js
  9. 14
      ui/src/app/components/widget-config.tpl.html
  10. 13
      ui/src/app/components/widget.controller.js
  11. 1
      ui/src/app/dashboard/add-widget.tpl.html
  12. 2
      ui/src/app/dashboard/dashboard.controller.js
  13. 5
      ui/src/app/dashboard/dashboard.tpl.html
  14. 3
      ui/src/app/dashboard/edit-widget.directive.js
  15. 1
      ui/src/app/dashboard/edit-widget.tpl.html
  16. 13
      ui/src/app/locale/locale.constant.js
  17. 474
      ui/src/app/widget/lib/entities-table-widget.js
  18. 58
      ui/src/app/widget/lib/entities-table-widget.scss
  19. 86
      ui/src/app/widget/lib/entities-table-widget.tpl.html
  20. 14
      ui/src/app/widget/widget-library.controller.js
  21. 1
      ui/src/app/widget/widget-library.tpl.html

28
ui/src/app/api/widget.service.js

@ -20,6 +20,7 @@ import tinycolor from 'tinycolor2';
import thingsboardLedLight from '../components/led-light.directive'; import thingsboardLedLight from '../components/led-light.directive';
import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget'; import thingsboardTimeseriesTableWidget from '../widget/lib/timeseries-table-widget';
import thingsboardAlarmsTableWidget from '../widget/lib/alarms-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 TbFlot from '../widget/lib/flot-widget';
import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge'; import TbAnalogueLinearGauge from '../widget/lib/analogue-linear-gauge';
@ -34,7 +35,7 @@ import thingsboardTypes from '../common/types.constant';
import thingsboardUtils from '../common/utils.service'; import thingsboardUtils from '../common/utils.service';
export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget, export default angular.module('thingsboard.api.widget', ['oc.lazyLoad', thingsboardLedLight, thingsboardTimeseriesTableWidget,
thingsboardAlarmsTableWidget, thingsboardTypes, thingsboardUtils]) thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardTypes, thingsboardUtils])
.factory('widgetService', WidgetService) .factory('widgetService', WidgetService)
.name; .name;
@ -546,6 +547,14 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
' }\n\n' + ' }\n\n' +
' self.typeParameters = function() {\n\n' +
{
useCustomDatasources: false,
maxDatasources: -1 //unlimited
maxDataKeys: -1 //unlimited
}
' }\n\n' +
' self.onResize = function() {\n\n' + ' self.onResize = function() {\n\n' +
' }\n\n' + ' }\n\n' +
@ -586,10 +595,21 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) { if (angular.isFunction(widgetTypeInstance.getDataKeySettingsSchema)) {
result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema(); result.dataKeySettingsSchema = widgetTypeInstance.getDataKeySettingsSchema();
} }
if (angular.isFunction(widgetTypeInstance.typeParameters)) {
result.typeParameters = widgetTypeInstance.typeParameters();
} else {
result.typeParameters = {};
}
if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) { if (angular.isFunction(widgetTypeInstance.useCustomDatasources)) {
result.useCustomDatasources = widgetTypeInstance.useCustomDatasources(); result.typeParameters.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
} else { } 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; return result;
} catch (e) { } catch (e) {
@ -629,7 +649,7 @@ function WidgetService($rootScope, $http, $q, $filter, $ocLazyLoad, $window, typ
if (widgetType.dataKeySettingsSchema) { if (widgetType.dataKeySettingsSchema) {
widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema; widgetInfo.typeDataKeySettingsSchema = widgetType.dataKeySettingsSchema;
} }
widgetInfo.useCustomDatasources = widgetType.useCustomDatasources; widgetInfo.typeParameters = widgetType.typeParameters;
putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem); putWidgetInfoToCache(widgetInfo, bundleAlias, widgetInfo.alias, isSystem);
putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem); putWidgetTypeFunctionToCache(widgetType.widgetTypeFunction, bundleAlias, widgetInfo.alias, isSystem);
deferred.resolve(widgetInfo); deferred.resolve(widgetInfo);

13
ui/src/app/components/datasource-entity.directive.js

@ -156,11 +156,19 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
}; };
scope.transformTimeseriesDataKeyChip = function (chip) { 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) { 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) { scope.transformAlarmDataKeyChip = function (chip) {
@ -272,6 +280,7 @@ function DatasourceEntity($compile, $templateCache, $q, $mdDialog, $window, $doc
require: "^ngModel", require: "^ngModel",
scope: { scope: {
widgetType: '=', widgetType: '=',
maxDataKeys: '=',
aliasController: '=', aliasController: '=',
datakeySettingsSchema: '=', datakeySettingsSchema: '=',
generateDataKey: '&', generateDataKey: '&',

5
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.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 translate ng-message="entityKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
</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>
</section> </section>

7
ui/src/app/components/datasource-func.directive.js

@ -117,7 +117,11 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
}; };
scope.transformFuncDataKeyChip = function (chip) { 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) { scope.transformAlarmDataKeyChip = function (chip) {
@ -217,6 +221,7 @@ function DatasourceFunc($compile, $templateCache, $mdDialog, $window, $document,
require: "^ngModel", require: "^ngModel",
scope: { scope: {
widgetType: '=', widgetType: '=',
maxDataKeys: '=',
generateDataKey: '&', generateDataKey: '&',
datakeySettingsSchema: '=' datakeySettingsSchema: '='
}, },

7
ui/src/app/components/datasource-func.tpl.html

@ -17,7 +17,7 @@
--> -->
<section class="tb-datasource-func" flex layout='column' <section class="tb-datasource-func" flex layout='column'
layout-align="center" layout-gt-sm='row' layout-align-gt-sm="start center"> 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;"> class="tb-datasource-name" md-no-float style="min-width: 200px;">
<input name="datasourceName" <input name="datasourceName"
placeholder="{{ 'datasource.name' | translate }}" 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.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 translate ng-message="datasourceKeys" ng-if="widgetType === types.widgetType.alarm.value" class="tb-error-message">datakey.alarm-fields-required</div>
</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>
</section> </section>

1
ui/src/app/components/datasource.directive.js

@ -82,6 +82,7 @@ function Datasource($compile, $templateCache, utils, types) {
require: "^ngModel", require: "^ngModel",
scope: { scope: {
aliasController: '=', aliasController: '=',
maxDataKeys: '=',
widgetType: '=', widgetType: '=',
functionsOnly: '=', functionsOnly: '=',
datakeySettingsSchema: '=', datakeySettingsSchema: '=',

2
ui/src/app/components/datasource.tpl.html

@ -27,6 +27,7 @@
<tb-datasource-func flex <tb-datasource-func flex
ng-switch-default ng-switch-default
ng-model="model" ng-model="model"
max-data-keys="maxDataKeys"
datakey-settings-schema="datakeySettingsSchema" datakey-settings-schema="datakeySettingsSchema"
ng-required="model.type === types.datasourceType.function" ng-required="model.type === types.datasourceType.function"
widget-type="widgetType" widget-type="widgetType"
@ -34,6 +35,7 @@
</tb-datasource-func> </tb-datasource-func>
<tb-datasource-entity flex <tb-datasource-entity flex
ng-model="model" ng-model="model"
max-data-keys="maxDataKeys"
datakey-settings-schema="datakeySettingsSchema" datakey-settings-schema="datakeySettingsSchema"
ng-switch-when="entity" ng-switch-when="entity"
ng-required="model.type === types.datasourceType.entity" ng-required="model.type === types.datasourceType.entity"

1
ui/src/app/components/widget-config.directive.js

@ -442,6 +442,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
forceExpandDatasources: '=?', forceExpandDatasources: '=?',
isDataEnabled: '=?', isDataEnabled: '=?',
widgetType: '=', widgetType: '=',
typeParameters: '=',
widgetSettingsSchema: '=', widgetSettingsSchema: '=',
datakeySettingsSchema: '=', datakeySettingsSchema: '=',
aliasController: '=', aliasController: '=',

14
ui/src/app/components/widget-config.tpl.html

@ -62,7 +62,14 @@
&& isDataEnabled"> && isDataEnabled">
<v-pane id="datasources-pane" expanded="true"> <v-pane id="datasources-pane" expanded="true">
<v-pane-header> <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-header>
<v-pane-content> <v-pane-content>
<div ng-if="datasources.length === 0"> <div ng-if="datasources.length === 0">
@ -88,6 +95,7 @@
style="padding: 0 0 0 10px; margin: 5px;"> style="padding: 0 0 0 10px; margin: 5px;">
<tb-datasource flex ng-model="datasource.value" <tb-datasource flex ng-model="datasource.value"
widget-type="widgetType" widget-type="widgetType"
max-data-keys="typeParameters.maxDataKeys"
alias-controller="aliasController" alias-controller="aliasController"
functions-only="functionsOnly" functions-only="functionsOnly"
datakey-settings-schema="datakeySettingsSchema" datakey-settings-schema="datakeySettingsSchema"
@ -111,7 +119,7 @@
</div> </div>
</div> </div>
<div flex layout="row" layout-align="start center"> <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 }}"> ng-click="addDatasource($event)" aria-label="{{ 'action.add' | translate }}">
<md-tooltip md-direction="top"> <md-tooltip md-direction="top">
{{ 'widget-config.add-datasource' | translate }} {{ 'widget-config.add-datasource' | translate }}
@ -140,7 +148,7 @@
</v-pane-content> </v-pane-content>
</v-pane> </v-pane>
</v-accordion> </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"> ng-if="widgetType === types.widgetType.alarm.value && isDataEnabled">
<v-pane id="alarm-source-pane" expanded="true"> <v-pane id="alarm-source-pane" expanded="true">
<v-pane-header> <v-pane-header>

13
ui/src/app/components/widget.controller.js

@ -128,7 +128,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var widgetTypeInstance; var widgetTypeInstance;
vm.useCustomDatasources = false; vm.typeParameters = widgetInfo.typeParameters;
try { try {
widgetTypeInstance = new widgetType(widgetContext); widgetTypeInstance = new widgetType(widgetContext);
@ -154,9 +154,6 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
if (!widgetTypeInstance.onDestroy) { if (!widgetTypeInstance.onDestroy) {
widgetTypeInstance.onDestroy = function() {}; widgetTypeInstance.onDestroy = function() {};
} }
if (widgetTypeInstance.useCustomDatasources) {
vm.useCustomDatasources = widgetTypeInstance.useCustomDatasources();
}
//TODO: widgets visibility //TODO: widgets visibility
@ -502,7 +499,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
var subscription = widgetContext.subscriptions[id]; var subscription = widgetContext.subscriptions[id];
subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds); subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds);
} }
if (subscriptionChanged && !vm.useCustomDatasources) { if (subscriptionChanged && !vm.typeParameters.useCustomDatasources) {
reInit(); reInit();
} }
}); });
@ -513,7 +510,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
configureWidgetElement(); configureWidgetElement();
var deferred = $q.defer(); var deferred = $q.defer();
if (!vm.useCustomDatasources) { if (!vm.typeParameters.useCustomDatasources) {
createDefaultSubscription().then( createDefaultSubscription().then(
function success() { function success() {
subscriptionInited = true; subscriptionInited = true;
@ -535,7 +532,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
function reInit() { function reInit() {
onDestroy(); onDestroy();
configureWidgetElement(); configureWidgetElement();
if (!vm.useCustomDatasources) { if (!vm.typeParameters.useCustomDatasources) {
createDefaultSubscription().then( createDefaultSubscription().then(
function success() { function success() {
subscriptionInited = true; subscriptionInited = true;
@ -575,7 +572,7 @@ export default function WidgetController($scope, $timeout, $window, $element, $q
} catch (e) { } catch (e) {
handleWidgetException(e); handleWidgetException(e);
} }
if (!vm.useCustomDatasources && widgetContext.defaultSubscription) { if (!vm.typeParameters.useCustomDatasources && widgetContext.defaultSubscription) {
widgetContext.defaultSubscription.subscribe(); widgetContext.defaultSubscription.subscribe();
} }
} }

1
ui/src/app/dashboard/add-widget.tpl.html

@ -33,6 +33,7 @@
<div class="md-dialog-content" style="padding-top: 0px;"> <div class="md-dialog-content" style="padding-top: 0px;">
<fieldset ng-disabled="loading" style="position: relative; height: 600px;"> <fieldset ng-disabled="loading" style="position: relative; height: 600px;">
<tb-widget-config widget-type="vm.widget.type" <tb-widget-config widget-type="vm.widget.type"
type-parameters="vm.widgetInfo.typeParameters"
force-expand-datasources="true" force-expand-datasources="true"
ng-model="vm.widgetConfig" ng-model="vm.widgetConfig"
widget-settings-schema="vm.settingsSchema" widget-settings-schema="vm.settingsSchema"

2
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); addWidget(newWidget);
} else { } else {
$mdDialog.show({ $mdDialog.show({

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

@ -212,6 +212,7 @@
class="tb-absolute-fill" md-border-bottom> class="tb-absolute-fill" md-border-bottom>
<md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}"> <md-tab ng-if="vm.timeseriesWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.timeseries' | translate }}">
<tb-dashboard <tb-dashboard
alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.timeseriesWidgetTypes" widgets="vm.timeseriesWidgetTypes"
is-edit="false" is-edit="false"
is-mobile="true" is-mobile="true"
@ -222,6 +223,7 @@
</md-tab> </md-tab>
<md-tab ng-if="vm.latestWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}"> <md-tab ng-if="vm.latestWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.latest-values' | translate }}">
<tb-dashboard <tb-dashboard
alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.latestWidgetTypes" widgets="vm.latestWidgetTypes"
is-edit="false" is-edit="false"
is-mobile="true" is-mobile="true"
@ -232,6 +234,7 @@
</md-tab> </md-tab>
<md-tab ng-if="vm.rpcWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.rpc' | translate }}"> <md-tab ng-if="vm.rpcWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.rpc' | translate }}">
<tb-dashboard <tb-dashboard
alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.rpcWidgetTypes" widgets="vm.rpcWidgetTypes"
is-edit="false" is-edit="false"
is-mobile="true" is-mobile="true"
@ -242,6 +245,7 @@
</md-tab> </md-tab>
<md-tab ng-if="vm.alarmWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.alarm' | translate }}"> <md-tab ng-if="vm.alarmWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.alarm' | translate }}">
<tb-dashboard <tb-dashboard
alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.alarmWidgetTypes" widgets="vm.alarmWidgetTypes"
is-edit="false" is-edit="false"
is-mobile="true" is-mobile="true"
@ -252,6 +256,7 @@
</md-tab> </md-tab>
<md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}"> <md-tab ng-if="vm.staticWidgetTypes.length > 0" style="height: 100%;" label="{{ 'widget.static' | translate }}">
<tb-dashboard <tb-dashboard
alias-controller="vm.dashboardCtx.aliasController"
widgets="vm.staticWidgetTypes" widgets="vm.staticWidgetTypes"
is-edit="false" is-edit="false"
is-mobile="true" is-mobile="true"

3
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 settingsSchema = widgetInfo.typeSettingsSchema || widgetInfo.settingsSchema;
var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema; var dataKeySettingsSchema = widgetInfo.typeDataKeySettingsSchema || widgetInfo.dataKeySettingsSchema;
scope.isDataEnabled = !widgetInfo.useCustomDatasources; scope.typeParameters = widgetInfo.typeParameters;
scope.isDataEnabled = !widgetInfo.typeParameters.useCustomDatasources;
if (!settingsSchema || settingsSchema === '') { if (!settingsSchema || settingsSchema === '') {
scope.settingsSchema = {}; scope.settingsSchema = {};
} else { } else {

1
ui/src/app/dashboard/edit-widget.tpl.html

@ -17,6 +17,7 @@
--> -->
<fieldset ng-disabled="loading"> <fieldset ng-disabled="loading">
<tb-widget-config widget-type="widget.type" <tb-widget-config widget-type="widget.type"
type-parameters="typeParameters"
ng-model="widgetConfig" ng-model="widgetConfig"
is-data-enabled="isDataEnabled" is-data-enabled="isDataEnabled"
widget-settings-schema="settingsSchema" widget-settings-schema="settingsSchema"

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

@ -499,9 +499,11 @@ export default angular.module('thingsboard.locale', [])
"alarm": "Alarm fields", "alarm": "Alarm fields",
"timeseries-required": "Entity timeseries are required.", "timeseries-required": "Entity timeseries are required.",
"timeseries-or-attributes-required": "Entity timeseries/attributes 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.", "alarm-fields-required": "Alarm fields are required.",
"function-types": "Function types", "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": { "datasource": {
"type": "Datasource type", "type": "Datasource type",
@ -691,7 +693,13 @@ export default angular.module('thingsboard.locale', [])
"type-alarm": "Alarm", "type-alarm": "Alarm",
"type-alarms": "Alarms", "type-alarms": "Alarms",
"list-of-alarms": "{ count, select, 1 {One alarms} other {List of # 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": {
"event-type": "Event type", "event-type": "Event type",
@ -1144,6 +1152,7 @@ export default angular.module('thingsboard.locale', [])
"use-dashboard-timewindow": "Use dashboard timewindow", "use-dashboard-timewindow": "Use dashboard timewindow",
"display-legend": "Display legend", "display-legend": "Display legend",
"datasources": "Datasources", "datasources": "Datasources",
"maximum-datasources": "Maximum { count, select, 1 {1 datasource is allowed.} other {# datasources are allowed} }",
"datasource-type": "Type", "datasource-type": "Type",
"datasource-parameters": "Parameters", "datasource-parameters": "Parameters",
"remove-datasource": "Remove datasource", "remove-datasource": "Remove datasource",

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

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

86
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>&nbsp;</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>&nbsp</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>

14
ui/src/app/widget/widget-library.controller.js

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import AliasController from '../api/alias-controller';
/* eslint-disable import/no-unresolved, import/default */ /* eslint-disable import/no-unresolved, import/default */
import selectWidgetTypeTemplate from './select-widget-type.tpl.html'; import selectWidgetTypeTemplate from './select-widget-type.tpl.html';
@ -21,7 +24,8 @@ import selectWidgetTypeTemplate from './select-widget-type.tpl.html';
/*@ngInject*/ /*@ngInject*/
export default function WidgetLibraryController($scope, $rootScope, $q, widgetService, userService, importExport, 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; var vm = this;
@ -31,6 +35,14 @@ export default function WidgetLibraryController($scope, $rootScope, $q, widgetSe
vm.widgetTypes = []; vm.widgetTypes = [];
vm.dashboardInitComplete = false; vm.dashboardInitComplete = false;
var stateController = {
getStateParams: function() {
return {};
}
};
vm.aliasController = new AliasController($scope, $q, $filter, utils,
types, entityService, stateController, {});
vm.noData = noData; vm.noData = noData;
vm.dashboardInited = dashboardInited; vm.dashboardInited = dashboardInited;
vm.dashboardInitFailed = dashboardInitFailed; vm.dashboardInitFailed = dashboardInitFailed;

1
ui/src/app/widget/widget-library.tpl.html

@ -28,6 +28,7 @@
class="md-headline tb-absolute-fill">widgets-bundle.empty</span> class="md-headline tb-absolute-fill">widgets-bundle.empty</span>
</section> </section>
<tb-dashboard <tb-dashboard
alias-controller="vm.aliasController"
widgets="vm.widgetTypes" widgets="vm.widgetTypes"
is-edit="false" is-edit="false"
is-edit-action-enabled="true" is-edit-action-enabled="true"

Loading…
Cancel
Save