Browse Source

Merge pull request #465 from thingsboard/feature/TB-74

TB-74: Ability to configure the gateway remotely
pull/473/head
Andrew Shvayka 9 years ago
committed by GitHub
parent
commit
40a59b7772
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 25
      application/src/main/data/json/system/widget_bundles/gateway_widgets.json
  2. 1
      ui/package.json
  3. 3
      ui/src/app/api/widget.service.js
  4. 3
      ui/src/app/app.js
  5. 47
      ui/src/app/common/types.constant.js
  6. 7
      ui/src/app/device/devices.tpl.html
  7. 4
      ui/src/app/entity/attribute/attribute-table.tpl.html
  8. 298
      ui/src/app/extension/extension-dialog.controller.js
  9. 81
      ui/src/app/extension/extension-dialog.tpl.html
  10. 397
      ui/src/app/extension/extension-table.directive.js
  11. 47
      ui/src/app/extension/extension-table.scss
  12. 159
      ui/src/app/extension/extension-table.tpl.html
  13. 147
      ui/src/app/extension/extensions-forms/extension-form-http.directive.js
  14. 311
      ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
  15. 347
      ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
  16. 860
      ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
  17. 173
      ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
  18. 557
      ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
  19. 76
      ui/src/app/extension/extensions-forms/extension-form.scss
  20. 29
      ui/src/app/extension/index.js
  21. 90
      ui/src/app/import-export/import-export.service.js
  22. 2
      ui/src/app/layout/index.js
  23. 131
      ui/src/app/locale/locale.constant.js
  24. 135
      ui/src/app/widget/lib/extensions-table-widget.js
  25. 29
      ui/src/app/widget/lib/extensions-table-widget.scss
  26. 27
      ui/src/app/widget/lib/extensions-table-widget.tpl.html

25
application/src/main/data/json/system/widget_bundles/gateway_widgets.json

@ -0,0 +1,25 @@
{
"widgetsBundle": {
"alias": "gateway_widgets",
"title": "Gateway widgets",
"image": null
},
"widgetTypes": [
{
"alias": "extension_configuration_widget",
"name": "Extensions table",
"descriptor": {
"type": "latest",
"sizeX": 9,
"sizeY": 6.5,
"resources": [],
"templateHtml": "<tb-extensions-table-widget \n ctx=\"ctx\">\n</tb-extensions-table-widget>",
"templateCss": "#container {\n overflow: auto;\n}",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n}\n\nself.onResize = function() {\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1\n };\n}\n\nself.onDestroy = function() {\n}\n",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"ExtensionTableSettings\",\n \"properties\": {\n \"extensionsTitle\": {\n \"title\": \"Extension table title\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"extensionsTitle\"\n ]\n}",
"dataKeySettingsSchema": "{}\n",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{},\"title\":\"Extensions table\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"18px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}"
}
}
]
}

1
ui/package.json

@ -30,6 +30,7 @@
"angular-material": "1.1.1",
"angular-material-data-table": "^0.10.9",
"angular-material-icons": "^0.7.1",
"angular-material-expansion-panel": "^0.7.2",
"angular-messages": "1.5.8",
"angular-route": "1.5.8",
"angular-sanitize": "1.5.8",

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

@ -21,6 +21,7 @@ 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 thingsboardExtensionsTableWidget from '../widget/lib/extensions-table-widget';
import thingsboardRpcWidgets from '../widget/lib/rpc';
@ -42,7 +43,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, thingsboardEntitiesTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
thingsboardAlarmsTableWidget, thingsboardEntitiesTableWidget, thingsboardExtensionsTableWidget, thingsboardRpcWidgets, thingsboardTypes, thingsboardUtils])
.factory('widgetService', WidgetService)
.name;

3
ui/src/app/app.js

@ -39,6 +39,7 @@ import uiRouter from 'angular-ui-router';
import angularJwt from 'angular-jwt';
import 'angular-drag-and-drop-lists';
import mdDataTable from 'angular-material-data-table';
import 'angular-material-expansion-panel';
import ngTouch from 'angular-touch';
import 'angular-carousel';
import 'clipboard';
@ -82,6 +83,7 @@ import 'md-color-picker/dist/mdColorPicker.min.css';
import 'mdPickers/dist/mdPickers.min.css';
import 'angular-hotkeys/build/hotkeys.min.css';
import 'angular-carousel/dist/angular-carousel.min.css';
import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
import '../scss/main.scss';
import AppConfig from './app.config';
@ -103,6 +105,7 @@ angular.module('thingsboard', [
angularJwt,
'dndLists',
mdDataTable,
'material.components.expansionPanels',
ngTouch,
'angular-carousel',
'ngclipboard',

47
ui/src/app/common/types.constant.js

@ -317,6 +317,53 @@ export default angular.module('thingsboard.types', [])
name: "event.type-stats"
}
},
extensionType: {
http: "HTTP",
mqtt: "MQTT",
opc: "OPC UA"
},
extensionValueType: {
string: 'value.string',
long: 'value.long',
double: 'value.double',
boolean: 'value.boolean'
},
extensionTransformerType: {
toDouble: 'extension.to-double',
custom: 'extension.custom'
},
mqttConverterTypes: {
json: 'extension.converter-json',
custom: 'extension.custom'
},
mqttCredentialTypes: {
anonymous: {
value: "anonymous",
name: "extension.anonymous"
},
basic: {
value: "basic",
name: "extension.basic"
},
pem: {
value: "cert.PEM",
name: "extension.pem"
}
},
extensionOpcSecurityTypes: {
Basic128Rsa15: "Basic128Rsa15",
Basic256: "Basic256",
Basic256Sha256: "Basic256Sha256",
None: "None"
},
extensionIdentityType: {
anonymous: "extension.anonymous",
username: "extension.username"
},
extensionKeystoreType: {
PKCS12: "PKCS12",
JKS: "JKS"
},
latestTelemetry: {
value: "LATEST_TELEMETRY",
name: "attribute.scope-latest-telemetry",

7
ui/src/app/device/devices.tpl.html

@ -67,4 +67,11 @@
entity-type="{{vm.types.entityType.device}}">
</tb-relation-table>
</md-tab>
<md-tab ng-if="!vm.grid.detailsConfig.isDetailsEditMode && vm.grid.operatingItem().additionalInfo.gateway" md-on-select="vm.grid.triggerResize()" label="{{ 'extension.extensions' | translate }}">
<tb-extension-table flex
entity-id="vm.grid.operatingItem().id.id"
entity-name="vm.grid.operatingItem().name"
entity-type="{{vm.types.entityType.device}}">
</tb-extension-table>
</md-tab>
</tb-grid>

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

@ -16,7 +16,7 @@
-->
<md-content flex class="md-padding tb-absolute-fill" layout="column">
<section layout="row" ng-show="!disableAttributeScopeSelection">
<section ng-show="!disableAttributeScopeSelection">
<md-input-container class="md-block" style="width: 200px;">
<label translate>attribute.attributes-scope</label>
<md-select ng-model="attributeScope" ng-disabled="loading() || attributeScopeSelectionReadonly">
@ -26,7 +26,7 @@
</md-select>
</md-input-container>
</section>
<div layout="column" class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
<div class="md-whiteframe-z1" ng-class="{flex: mode==='widget'}">
<md-toolbar class="md-table-toolbar md-default" ng-show="mode==='default'
&& !selectedAttributes.length
&& query.search === null">

298
ui/src/app/extension/extension-dialog.controller.js

@ -0,0 +1,298 @@
/*
* 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 beautify from 'js-beautify';
const js_beautify = beautify.js;
/*@ngInject*/
export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) {
var vm = this;
vm.types = types;
vm.isAdd = isAdd;
vm.entityType = entityType;
vm.entityId = entityId;
vm.allExtensions = allExtensions;
if (extension) {
vm.extension = angular.copy(extension);
editTransformers(vm.extension);
} else {
vm.extension = {};
}
vm.extensionTypeChange = function () {
if (vm.extension.type === "HTTP") {
vm.extension.configuration = {
"converterConfigurations": []
};
}
if (vm.extension.type === "MQTT") {
vm.extension.configuration = {
"brokers": []
};
}
if (vm.extension.type === "OPC UA") {
vm.extension.configuration = {
"servers": []
};
}
};
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
vm.save = save;
function save() {
let $errorElement = angular.element('[name=theForm]').find('.ng-invalid');
if ($errorElement.length) {
let $mdDialogScroll = angular.element('md-dialog-content').scrollTop();
let $mdDialogTop = angular.element('md-dialog-content').offset().top;
let $errorElementTop = angular.element('[name=theForm]').find('.ng-invalid').eq(0).offset().top;
if ($errorElementTop !== $mdDialogTop) {
angular.element('md-dialog-content').animate({
scrollTop: $mdDialogScroll + ($errorElementTop - $mdDialogTop) - 50
}, 500);
$errorElement.eq(0).focus();
}
} else {
if(vm.isAdd) {
vm.allExtensions.push(vm.extension);
} else {
var index = vm.allExtensions.indexOf(extension);
if(index > -1) {
vm.allExtensions[index] = vm.extension;
}
}
$mdDialog.hide();
saveTransformers();
var editedValue = angular.toJson(vm.allExtensions);
attributeService
.saveEntityAttributes(
vm.entityType,
vm.entityId,
types.attributesScope.shared.value,
[{key:"configuration", value:editedValue}]
)
.then(function success() {
});
}
}
vm.validateId = function() {
var coincidenceArray = vm.allExtensions.filter(function(ext) {
return ext.id == vm.extension.id;
});
if(coincidenceArray.length) {
if(!vm.isAdd) {
if(coincidenceArray[0].id == extension.id) {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
}
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
}
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
}
};
function saveTransformers() {
if(vm.extension.type == types.extensionType.http) {
var config = vm.extension.configuration.converterConfigurations;
if(config && config.length > 0) {
for(let i=0;i<config.length;i++) {
for(let j=0;j<config[i].converters.length;j++){
for(let k=0;k<config[i].converters[j].attributes.length;k++){
if(config[i].converters[j].attributes[k].transformerType == "toDouble"){
config[i].converters[j].attributes[k].transformer = {type: "intToDouble"};
}
delete config[i].converters[j].attributes[k].transformerType;
}
for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
if(config[i].converters[j].timeseries[l].transformerType == "toDouble"){
config[i].converters[j].timeseries[l].transformer = {type: "intToDouble"};
}
delete config[i].converters[j].timeseries[l].transformerType;
}
}
}
}
}
if(vm.extension.type == types.extensionType.mqtt) {
var brokers = vm.extension.configuration.brokers;
if(brokers && brokers.length > 0) {
for(let i=0;i<brokers.length;i++) {
if(brokers[i].mapping && brokers[i].mapping.length > 0) {
for(let j=0;j<brokers[i].mapping.length;j++) {
if(brokers[i].mapping[j].converterType == "json") {
delete brokers[i].mapping[j].converter.nameExp;
delete brokers[i].mapping[j].converter.typeExp;
}
delete brokers[i].mapping[j].converterType;
}
}
if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
for(let j=0;j<brokers[i].connectRequests.length;j++) {
delete brokers[i].connectRequests[j].nameExp;
}
}
if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
delete brokers[i].disconnectRequests[j].nameExp;
}
}
if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
for(let j=0;j<brokers[i].attributeRequests.length;j++) {
delete brokers[i].attributeRequests[j].nameExp;
}
for(let j=0;j<brokers[i].attributeRequests.length;j++) {
delete brokers[i].attributeRequests[j].attrKey;
}
for(let j=0;j<brokers[i].attributeRequests.length;j++) {
delete brokers[i].attributeRequests[j].requestId;
}
}
}
}
}
}
function editTransformers(extension) {
if(extension.type == types.extensionType.http) {
var config = extension.configuration.converterConfigurations;
for(let i=0;i<config.length;i++) {
for(let j=0;j<config[i].converters.length;j++){
for(let k=0;k<config[i].converters[j].attributes.length;k++){
if(config[i].converters[j].attributes[k].transformer){
if(config[i].converters[j].attributes[k].transformer.type == "intToDouble"){
config[i].converters[j].attributes[k].transformerType = "toDouble";
} else {
config[i].converters[j].attributes[k].transformerType = "custom";
config[i].converters[j].attributes[k].transformer = js_beautify(config[i].converters[j].attributes[k].transformer, {indent_size: 4});
}
}
}
for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
if(config[i].converters[j].timeseries[l].transformer){
if(config[i].converters[j].timeseries[l].transformer.type == "intToDouble"){
config[i].converters[j].timeseries[l].transformerType = "toDouble";
} else {
config[i].converters[j].timeseries[l].transformerType = "custom";
config[i].converters[j].timeseries[l].transformer = js_beautify(config[i].converters[j].timeseries[l].transformer, {indent_size: 4});
}
}
}
}
}
}
if(extension.type == types.extensionType.mqtt) {
var brokers = extension.configuration.brokers;
for(let i=0;i<brokers.length;i++) {
if(brokers[i].mapping && brokers[i].mapping.length > 0) {
for(let j=0;j<brokers[i].mapping.length;j++) {
if(brokers[i].mapping[j].converter.type == "json") {
if(brokers[i].mapping[j].converter.deviceNameTopicExpression) {
brokers[i].mapping[j].converter.nameExp = "deviceNameTopicExpression";
} else {
brokers[i].mapping[j].converter.nameExp = "deviceNameJsonExpression";
}
if(brokers[i].mapping[j].converter.deviceTypeTopicExpression) {
brokers[i].mapping[j].converter.typeExp = "deviceTypeTopicExpression";
} else {
brokers[i].mapping[j].converter.typeExp = "deviceTypeJsonExpression";
}
brokers[i].mapping[j].converterType = "json";
} else {
brokers[i].mapping[j].converterType = "custom";
}
}
}
if(brokers[i].connectRequests && brokers[i].connectRequests.length > 0) {
for(let j=0;j<brokers[i].connectRequests.length;j++) {
if(brokers[i].connectRequests[j].deviceNameTopicExpression) {
brokers[i].connectRequests[j].nameExp = "deviceNameTopicExpression";
} else {
brokers[i].connectRequests[j].nameExp = "deviceNameJsonExpression";
}
}
}
if(brokers[i].disconnectRequests && brokers[i].disconnectRequests.length > 0) {
for(let j=0;j<brokers[i].disconnectRequests.length;j++) {
if(brokers[i].disconnectRequests[j].deviceNameTopicExpression) {
brokers[i].disconnectRequests[j].nameExp = "deviceNameTopicExpression";
} else {
brokers[i].disconnectRequests[j].nameExp = "deviceNameJsonExpression";
}
}
}
if(brokers[i].attributeRequests && brokers[i].attributeRequests.length > 0) {
for(let j=0;j<brokers[i].attributeRequests.length;j++) {
if(brokers[i].attributeRequests[j].deviceNameTopicExpression) {
brokers[i].attributeRequests[j].nameExp = "deviceNameTopicExpression";
} else {
brokers[i].attributeRequests[j].nameExp = "deviceNameJsonExpression";
}
if(brokers[i].attributeRequests[j].attributeKeyTopicExpression) {
brokers[i].attributeRequests[j].attrKey = "attributeKeyTopicExpression";
} else {
brokers[i].attributeRequests[j].attrKey = "attributeKeyJsonExpression";
}
if(brokers[i].attributeRequests[j].requestIdTopicExpression) {
brokers[i].attributeRequests[j].requestId = "requestIdTopicExpression";
} else {
brokers[i].attributeRequests[j].requestId = "requestIdJsonExpression";
}
}
}
}
}
}
}
/*@ngInject*/
export function ParseToNull() {
var linker = function (scope, elem, attrs, ngModel) {
ngModel.$parsers.push(function(value) {
if(value === "") {
return null;
}
return value;
})
};
return {
restrict: "A",
link: linker,
require: "ngModel"
}
}

81
ui/src/app/extension/extension-dialog.tpl.html

@ -0,0 +1,81 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-dialog class="extensionDialog" aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}">
<form name="theForm" ng-submit="vm.save()" novalidate>
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>{{ vm.isAdd ? 'extension.add' : 'extension.edit'}}</h2>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!loading" ng-show="loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<md-content class="md-padding" layout="column">
<fieldset ng-disabled="loading">
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm.extensionId.$touched && theForm.extensionId.$invalid">
<label translate>extension.extension-id</label>
<input required name="extensionId" ng-model="vm.extension.id" ng-change="vm.validateId()">
<div ng-messages="theForm.extensionId.$error">
<div translate ng-message="required">extension.field-required</div>
<div translate ng-message="uniqueIdValidation">extension.unique-id-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm.extensionType.$touched && theForm.extensionType.$invalid">
<label translate>extension.extension-type</label>
<md-select ng-disabled="!vm.isAdd" required name="extensionType" ng-change="vm.extensionTypeChange()" ng-model="vm.extension.type">
<md-option ng-repeat="(key,value) in vm.types.extensionType" ng-value="value">
{{value}}
</md-option>
</md-select>
<div ng-messages="theForm.extensionType.$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<div tb-extension-form-http config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.http"></div>
<div tb-extension-form-mqtt config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.mqtt"></div>
<div tb-extension-form-opc configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
</fieldset>
</md-content>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<md-button type="submit"
class="md-raised md-primary"
>
{{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
</md-button>
<md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

397
ui/src/app/extension/extension-table.directive.js

@ -0,0 +1,397 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'angular-material-data-table/dist/md-data-table.min.css';
import './extension-table.scss';
/* eslint-disable import/no-unresolved, import/default */
import extensionTableTemplate from './extension-table.tpl.html';
import extensionDialogTemplate from './extension-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import ExtensionDialogController from './extension-dialog.controller'
import $ from 'jquery';
/*@ngInject*/
export default function ExtensionTableDirective() {
return {
restrict: "E",
scope: true,
bindToController: {
entityId: '=',
entityType: '@',
inWidget: '@?',
ctx: '=?',
entityName: '='
},
controller: ExtensionTableController,
controllerAs: 'vm',
templateUrl: extensionTableTemplate
};
}
/*@ngInject*/
function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService, telemetryWebsocketService, importExport) {
let vm = this;
vm.extensions = [];
vm.allExtensions = [];
vm.selectedExtensions = [];
vm.extensionsCount = 0;
vm.query = {
order: 'id',
limit: 5,
page: 1,
search: null
};
vm.enterFilterMode = enterFilterMode;
vm.exitFilterMode = exitFilterMode;
vm.onReorder = onReorder;
vm.onPaginate = onPaginate;
vm.addExtension = addExtension;
vm.editExtension = editExtension;
vm.deleteExtension = deleteExtension;
vm.deleteExtensions = deleteExtensions;
vm.reloadExtensions = reloadExtensions;
vm.updateExtensions = updateExtensions;
$scope.$watch("vm.entityId", function(newVal) {
if (newVal) {
if ($scope.subscriber) {
telemetryWebsocketService.unsubscribe($scope.subscriber);
}
vm.subscribed = false;
vm.syncLastTime = $translate.instant('extension.sync.not-available');
subscribeForClientAttributes();
reloadExtensions();
}
});
$scope.$watch("vm.query.search", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
updateExtensions();
}
});
$scope.$watch('vm.selectedExtensions.length', function (newLength) {
var selectionMode = newLength ? true : false;
if (vm.ctx) {
if (selectionMode) {
vm.ctx.hideTitlePanel = true;
$scope.$emit("selectedExtensions", true);
} else if (vm.query.search == null) {
vm.ctx.hideTitlePanel = false;
$scope.$emit("selectedExtensions", false);
}
}
});
$scope.$on("showSearch", function($event, source) {
if(source.entityId == vm.entityId) {
enterFilterMode();
$scope.$emit("filterMode", true);
}
});
$scope.$on("refreshExtensions", function($event, source) {
if(source.entityId == vm.entityId) {
reloadExtensions();
}
});
$scope.$on("addExtension", function($event, source) {
if(source.entityId == vm.entityId) {
addExtension();
}
});
$scope.$on("exportExtensions", function($event, source) {
if(source.entityId == vm.entityId) {
vm.exportExtensions(source.entityName);
}
});
$scope.$on("importExtensions", function($event, source) {
if(source.entityId == vm.entityId) {
vm.importExtensions();
}
});
function enterFilterMode() {
vm.query.search = '';
if(vm.inWidget) {
vm.ctx.hideTitlePanel = true;
}
}
function exitFilterMode() {
vm.query.search = null;
updateExtensions();
if(vm.inWidget) {
vm.ctx.hideTitlePanel = false;
$scope.$emit("filterMode", false);
}
}
function onReorder() {
updateExtensions();
}
function onPaginate() {
updateExtensions();
}
function addExtension($event) {
if ($event) {
$event.stopPropagation();
}
openExtensionDialog($event);
}
function editExtension($event, extension) {
if ($event) {
$event.stopPropagation();
}
openExtensionDialog($event, extension);
}
function openExtensionDialog($event, extension) {
if ($event) {
$event.stopPropagation();
}
var isAdd = false;
if(!extension) {
isAdd = true;
}
$mdDialog.show({
controller: ExtensionDialogController,
controllerAs: 'vm',
templateUrl: extensionDialogTemplate,
parent: angular.element($document[0].body),
locals: {
isAdd: isAdd,
allExtensions: vm.allExtensions,
entityId: vm.entityId,
entityType: vm.entityType,
extension: extension
},
bindToController: true,
targetEvent: $event,
fullscreen: true,
skipHide: true
}).then(function() {
reloadExtensions();
}, function () {
});
}
function deleteExtension($event, extension) {
if ($event) {
$event.stopPropagation();
}
if(extension) {
var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id});
var content = $translate.instant('extension.delete-extension-text');
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title(title)
.htmlContent(content)
.ariaLabel(title)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function() {
var editedExtensions = vm.allExtensions.filter(function(ext) {
return ext.id !== extension.id;
});
var editedValue = angular.toJson(editedExtensions);
attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
function success() {
reloadExtensions();
}
);
});
}
}
function deleteExtensions($event) {
if ($event) {
$event.stopPropagation();
}
if (vm.selectedExtensions && vm.selectedExtensions.length > 0) {
var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat');
var content = $translate.instant('extension.delete-extensions-text');
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title(title)
.htmlContent(content)
.ariaLabel(title)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
var editedExtensions = angular.copy(vm.allExtensions);
for (var i = 0; i < vm.selectedExtensions.length; i++) {
editedExtensions = editedExtensions.filter(function (ext) {
return ext.id !== vm.selectedExtensions[i].id;
});
}
var editedValue = angular.toJson(editedExtensions);
attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
function success() {
reloadExtensions();
}
);
});
}
}
function reloadExtensions() {
vm.subscribed = false;
vm.allExtensions.length = 0;
vm.extensions.length = 0;
vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]);
vm.extensionsPromise.then(
function success(data) {
if (data.length) {
vm.allExtensions = angular.fromJson(data[0].value);
} else {
vm.allExtensions = [];
}
vm.selectedExtensions = [];
updateExtensions();
vm.extensionsPromise = null;
},
function fail() {
vm.extensions = [];
vm.selectedExtensions = [];
updateExtensions();
vm.extensionsPromise = null;
}
);
}
function updateExtensions() {
vm.selectedExtensions = [];
var result = $filter('orderBy')(vm.allExtensions, vm.query.order);
if (vm.query.search != null) {
result = $filter('filter')(result, function(extension) {
if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) {
return true;
}
return false;
});
}
vm.extensionsCount = result.length;
var startIndex = vm.query.limit * (vm.query.page - 1);
vm.extensions = result.slice(startIndex, startIndex + vm.query.limit);
vm.extensionsJSON = angular.toJson(vm.extensions);
checkForSync();
}
function subscribeForClientAttributes() {
if (!vm.subscribed) {
if (vm.entityId && vm.entityType) {
$scope.subscriber = {
subscriptionCommands: [{
entityType: vm.entityType,
entityId: vm.entityId,
scope: 'CLIENT_SCOPE'
}],
type: 'attribute',
onData: function (data) {
if (data.data) {
onSubscriptionData(data.data);
}
vm.subscribed = true;
}
};
}
telemetryWebsocketService.subscribe($scope.subscriber);
}
}
function onSubscriptionData(data) {
if ($.isEmptyObject(data)) {
vm.appliedConfiguration = undefined;
} else {
if (data.appliedConfiguration && data.appliedConfiguration[0] && data.appliedConfiguration[0][1]) {
vm.appliedConfiguration = data.appliedConfiguration[0][1];
}
}
updateExtensions();
$scope.$digest();
}
function checkForSync() {
if (vm.appliedConfiguration && vm.extensionsJSON && vm.appliedConfiguration === vm.extensionsJSON) {
vm.syncStatus = $translate.instant('extension.sync.sync');
vm.syncLastTime = formatDate();
$scope.isSync = true;
} else {
vm.syncStatus = $translate.instant('extension.sync.not-sync');
$scope.isSync = false;
}
}
function formatDate(date) {
let d;
if (date) {
d = date;
} else {
d = new Date();
}
d = d.getFullYear() +'/'+ addZero(d.getMonth()+1) +'/'+ addZero(d.getDate()) + ' ' + addZero(d.getHours()) + ':' + addZero(d.getMinutes()) +':'+ addZero(d.getSeconds());
return d;
function addZero(num) {
if ((angular.isNumber(num) && num < 10) || (angular.isString(num) && num.length === 1)) {
num = '0' + num;
}
return num;
}
}
vm.importExtensions = function($event) {
importExport.importExtension($event, {"entityType":vm.entityType, "entityId":vm.entityId, "successFunc":reloadExtensions});
};
vm.exportExtensions = function(widgetSourceEntityName) {
if(vm.inWidget) {
importExport.exportToPc(vm.extensionsJSON, widgetSourceEntityName + '_configuration.json');
} else {
importExport.exportToPc(vm.extensionsJSON, vm.entityName + '_configuration.json');
}
};
/*change function for widget implementing, like vm.exportExtensions*/
vm.exportExtension = function($event, extension) {
if ($event) {
$event.stopPropagation();
}
importExport.exportToPc(extension, vm.entityName +'_'+ extension.id +'_configuration.json');
};
}

47
ui/src/app/extension/extension-table.scss

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../scss/constants';
.extension-table {
md-input-container .md-errors-spacer {
min-height: 0;
}
/*&.tb-data-table table.md-table tbody tr td.tb-action-cell,
&.tb-data-table table.md-table.md-row-select tbody tr td.tb-action-cell {
width: 114px;
}*/
.sync-widget {
max-height: 90px;
overflow: hidden;
}
.toolbar-widget {
min-height: 39px;
max-height: 39px;
}
}
.extension__syncStatus--black {
color: #000000!important;
}
.extension__syncStatus--green {
color: #228634!important;
}
.extension__syncStatus--red {
color: #862222!important;
}

159
ui/src/app/extension/extension-table.tpl.html

@ -0,0 +1,159 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-content flex class="md-padding tb-absolute-fill tb-data-table extension-table" layout="column">
<div layout="column" class="md-whiteframe-z1" ng-class="{'tb-absolute-fill' : vm.inWidget}">
<md-toolbar ng-if="!vm.inWidget" class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
&& vm.query.search === null">
<div class="md-toolbar-tools">
<span translate>{{ 'extension.extensions' }}</span>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.importExtensions($event)">
<md-icon>file_upload</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.import-extensions-configuration' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="vm.exportExtensions()">
<md-icon>file_download</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.export-extensions-configuration' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="vm.addExtension($event)">
<md-icon>add</md-icon>
<md-tooltip md-direction="top">
{{ 'action.add' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="vm.enterFilterMode()">
<md-icon>search</md-icon>
<md-tooltip md-direction="top">
{{ 'action.search' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" ng-click="vm.reloadExtensions()">
<md-icon>refresh</md-icon>
<md-tooltip md-direction="top">
{{ 'action.refresh' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<md-toolbar class="md-table-toolbar md-default" ng-show="!vm.selectedExtensions.length
&& vm.query.search != null" ng-class="{'toolbar-widget' : vm.inWidget}">
<div class="md-toolbar-tools">
<md-button class="md-icon-button" aria-label="{{ 'action.search' | translate }}">
<md-icon aria-label="{{ 'action.search' | translate }}" class="material-icons">search</md-icon>
<md-tooltip md-direction="top">
{{ 'action.search' | translate }}
</md-tooltip>
</md-button>
<md-input-container flex>
<label>&nbsp;</label>
<input ng-model="vm.query.search" placeholder="{{ 'common.enter-search' | translate }}"/>
</md-input-container>
<md-button class="md-icon-button" aria-label="{{ 'action.back' | translate }}" ng-click="vm.exitFilterMode()">
<md-icon aria-label="{{ 'action.close' | translate }}" class="material-icons">close</md-icon>
<md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
{{ 'action.close' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<md-toolbar class="md-table-toolbar alternate" ng-show="vm.selectedExtensions.length" ng-class="{'toolbar-widget' : vm.inWidget}">
<div class="md-toolbar-tools">
<span translate
translate-values="{count: vm.selectedExtensions.length}"
translate-interpolation="messageformat">extension.selected-extensions</span>
<span flex></span>
<md-button class="md-icon-button" ng-click="vm.deleteExtensions($event)">
<md-icon>delete</md-icon>
<md-tooltip md-direction="{{vm.ctx.dashboard.isWidgetExpanded ? 'bottom' : 'top'}}">
{{ 'action.delete' | translate }}
</md-tooltip>
</md-button>
</div>
</md-toolbar>
<div class="md-padding" flex layout="row" ng-class="{'sync-widget' : vm.inWidget}">
<md-input-container flex="50" class="md-block">
<label translate>extension.sync.status</label>
<input ng-model="vm.syncStatus"
ng-class="{'extension__syncStatus--green':isSync, 'extension__syncStatus--red':!isSync}"
disabled
>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.sync.last-sync-time</label>
<input ng-model="vm.syncLastTime"
class="extension__syncStatus--black"
disabled
>
</md-input-container>
</div>
<md-table-container flex>
<table md-table md-row-select multiple="" ng-model="vm.selectedExtensions" md-progress="vm.extensionsDeferred.promise">
<thead md-head md-order="vm.query.order" md-on-reorder="vm.onReorder">
<tr md-row>
<th md-column md-order-by="id"><span translate>extension.id</span></th>
<th md-column md-order-by="type"><span translate>extension.type</span></th>
<th md-column><span>&nbsp</span></th>
</tr>
</thead>
<tbody md-body>
<tr md-row md-select="extension" md-select-id="extension" md-auto-select ng-repeat="extension in vm.extensions">
<td md-cell>{{ extension.id }}</td>
<td md-cell>{{ extension.type }}</td>
<td md-cell class="tb-action-cell">
<!--<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.exportExtension($event, extension)">
<md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">file_download</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.export-extension' | translate }}
</md-tooltip>
</md-button>-->
<md-button class="md-icon-button" aria-label="{{ 'action.edit' | translate }}" ng-click="vm.editExtension($event, extension)">
<md-icon aria-label="{{ 'action.edit' | translate }}" class="material-icons">edit</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.edit' | translate }}
</md-tooltip>
</md-button>
<md-button class="md-icon-button" aria-label="{{ 'action.delete' | translate }}" ng-click="vm.deleteExtension($event, extension)">
<md-icon aria-label="{{ 'action.delete' | translate }}" class="material-icons">delete</md-icon>
<md-tooltip md-direction="top">
{{ 'extension.delete' | translate }}
</md-tooltip>
</md-button>
</td>
</tr>
</tbody>
</table>
<md-divider ng-if="vm.inWidget"></md-divider>
</md-table-container>
<md-table-pagination md-limit="vm.query.limit" md-limit-options="[5, 10, 15]"
md-page="vm.query.page" md-total="{{vm.extensionsCount}}"
md-on-paginate="vm.onPaginate" md-page-select>
</md-table-pagination>
</div>
</md-content>

147
ui/src/app/extension/extensions-forms/extension-form-http.directive.js

@ -0,0 +1,147 @@
/*
* 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 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/theme/github';
import './extension-form.scss';
/* eslint-disable angular/log */
import extensionFormHttpTemplate from './extension-form-http.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
var linker = function(scope, element) {
var template = $templateCache.get(extensionFormHttpTemplate);
element.html(template);
scope.types = types;
scope.theForm = scope.$parent.theForm;
scope.extensionCustomTransformerOptions = {
useWrapMode: false,
mode: 'json',
showGutter: true,
showPrintMargin: true,
theme: 'github',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function(_ace) {
_ace.$blockScrolling = 1;
}
};
scope.addConverterConfig = function() {
var newConverterConfig = {converterId:"", converters:[]};
scope.converterConfigs.push(newConverterConfig);
scope.converterConfigs[scope.converterConfigs.length - 1].converters = [];
scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters);
};
scope.removeConverterConfig = function(config) {
var index = scope.converterConfigs.indexOf(config);
if (index > -1) {
scope.converterConfigs.splice(index, 1);
}
};
scope.addConverter = function(converters) {
var newConverter = {
deviceNameJsonExpression:"",
deviceTypeJsonExpression:"",
attributes:[],
timeseries:[]
};
converters.push(newConverter);
};
scope.removeConverter = function(converter, converters) {
var index = converters.indexOf(converter);
if (index > -1) {
converters.splice(index, 1);
}
};
scope.addAttribute = function(attributes) {
var newAttribute = {type:"", key:"", value:""};
attributes.push(newAttribute);
};
scope.removeAttribute = function(attribute, attributes) {
var index = attributes.indexOf(attribute);
if (index > -1) {
attributes.splice(index, 1);
}
};
if(scope.isAdd) {
scope.converterConfigs = scope.config.converterConfigurations;
scope.addConverterConfig();
} else {
scope.converterConfigs = scope.config.converterConfigurations;
}
scope.transformerTypeChange = function(attribute) {
attribute.transformer = "";
};
scope.validateTransformer = function (model, editorName) {
if(model && model.length) {
try {
angular.fromJson(model);
scope.theForm[editorName].$setValidity('transformerJSON', true);
} catch(e) {
scope.theForm[editorName].$setValidity('transformerJSON', false);
}
}
};
scope.collapseValidation = function(index, id) {
var invalidState = angular.element('#'+id+':has(.ng-invalid)');
if(invalidState.length) {
invalidState.addClass('inner-invalid');
}
};
scope.expandValidation = function (index, id) {
var invalidState = angular.element('#'+id);
invalidState.removeClass('inner-invalid');
};
$compile(element.contents())(scope);
};
return {
restrict: "A",
link: linker,
scope: {
config: "=",
isAdd: "="
}
}
}

311
ui/src/app/extension/extensions-forms/extension-form-http.tpl.html

@ -0,0 +1,311 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-card class="extension-form extension-http">
<md-card-title>
<md-card-title-text>
<span translate class="md-headline">extension.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<v-accordion id="http-converter-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="http-converters-pane" expanded="true">
<v-pane-header>
{{ 'extension.converter-configurations' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="converterConfigs.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(configIndex, config) in converterConfigs">
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeConverterConfig(config)"
ng-hide="converterConfigs.length < 2"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<md-input-container class="md-block" md-is-error="theForm['httpConverterId_' + configIndex].$touched && theForm['httpConverterId_' + configIndex].$invalid">
<label translate>extension.converter-id</label>
<input required name="httpConverterId_{{configIndex}}" ng-model="config.converterId">
<div ng-messages="theForm['httpConverterId_' + configIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>extension.token</label>
<input name="httpToken" ng-model="config.token" parse-to-null>
</md-input-container>
<v-accordion id="http-converters-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="http-converters-pane_{{configIndex}}" expanded="true">
<v-pane-header>
{{ 'extension.converters' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="config.converters.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(converterIndex,converter) in config.converters"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeConverter(converter, config.converters)"
ng-hide="config.converters.length < 2"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<md-input-container class="md-block" md-is-error="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceNameExp_' + configIndex + converterIndex].$invalid">
<label translate>extension.device-name-expression</label>
<input required name="httpDeviceNameExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceNameJsonExpression">
<div ng-messages="theForm['httpDeviceNameExp_' + configIndex + converterIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block" md-is-error="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$touched && theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$invalid">
<label translate>extension.device-type-expression</label>
<input required name="httpDeviceTypeExp_{{configIndex}}{{converterIndex}}" ng-model="converter.deviceTypeJsonExpression">
<div ng-messages="theForm['httpDeviceTypeExp_' + configIndex + converterIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<v-accordion id="http-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="http-attributes-pane_{{configIndex}}{{converterIndex}}">
<v-pane-header>
{{ 'extension.attributes' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="converter.attributes.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(attributeIndex, attribute) in converter.attributes">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, converter.attributes)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$invalid">
<label translate>extension.key</label>
<input required name="httpAttributeKey_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.key">
<div ng-messages="theForm['httpAttributeKey_' + configIndex + converterIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$invalid">
<label translate>extension.type</label>
<md-select required name="httpAttributeType_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.type">
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$touched && theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$invalid">
<label translate>extension.value</label>
<input required name="httpAttributeValue_{{configIndex}}{{converterIndex}}{{attributeIndex}}" ng-model="attribute.value">
<div ng-messages="theForm['httpAttributeValue_' + configIndex + converterIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block">
<label translate>extension.transformer</label>
<md-select name="httpAttributeTransformer" ng-model="attribute.transformerType" ng-change="transformerTypeChange(attribute)">
<md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
{{value | translate}}
</md-option>
</md-select>
</md-input-container>
</section>
<div ng-if='attribute.transformerType == "custom"'>
<div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
<div flex class="tb-extension-custom-transformer-panel">
<div flex class="tb-extension-custom-transformer"
ui-ace="extensionCustomTransformerOptions"
ng-model="attribute.transformer"
name="attributeCustomTransformer_{{configIndex}}{{converterIndex}}{{attributeIndex}}"
ng-change='validateTransformer(attribute.transformer,"attributeCustomTransformer_" + configIndex + converterIndex + attributeIndex)'
required>
</div>
</div>
<div class="tb-error-messages" ng-messages="theForm['attributeCustomTransformer_' + configIndex + converterIndex + attributeIndex].$error" role="alert">
<div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
<div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
</div>
</div>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttribute(converter.attributes)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="http-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="http-timeseries-pane_{{configIndex}}{{converterIndex}}">
<v-pane-header>
{{ 'extension.timeseries' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="converter.timeseries.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in converter.timeseries">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, converter.timeseries)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$invalid">
<label translate>extension.key</label>
<input required name="httpTimeseriesKey_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
<div ng-messages="theForm['httpTimeseriesKey_' + configIndex + converterIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$invalid">
<label translate>extension.type</label>
<md-select required name="httpTimeseriesType_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$touched && theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$invalid">
<label translate>extension.value</label>
<input required name="httpTimeseriesValue_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
<div ng-messages="theForm['httpTimeseriesValue_' + configIndex + converterIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block">
<label translate>extension.transformer</label>
<md-select name="httpTimeseriesTransformer" ng-model="timeseries.transformerType" ng-change="transformerTypeChange(timeseries)">
<md-option ng-repeat="(transformerType, value) in types.extensionTransformerType" ng-value="transformerType">
{{value | translate}}
</md-option>
</md-select>
</md-input-container>
</section>
<div ng-if='timeseries.transformerType == "custom"'>
<div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
<div flex class="tb-extension-custom-transformer-panel">
<div flex class="tb-extension-custom-transformer"
ui-ace="extensionCustomTransformerOptions"
ng-model="timeseries.transformer"
name="timeseriesCustomTransformer_{{configIndex}}{{converterIndex}}{{timeseriesIndex}}"
ng-change='validateTransformer(timeseries.transformer,"timeseriesCustomTransformer_" + configIndex + converterIndex + timeseriesIndex)'
required>
</div>
</div>
<div class="tb-error-messages" ng-messages="theForm['timeseriesCustomTransformer_' + configIndex + converterIndex + timeseriesIndex].$error" role="alert">
<div ng-message="required" class="tb-error-message" translate>extension.json-required</div>
<div ng-message="transformerJSON" class="tb-error-message" translate>extension.json-parse</div>
</div>
</div>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttribute(converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addConverter(config.converters)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-converter</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addConverterConfig()" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-config</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<!--{{config}}-->
</md-card-content>
</md-card>

347
ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js

@ -0,0 +1,347 @@
/*
* 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 './extension-form.scss';
/* eslint-disable angular/log */
import extensionFormMqttTemplate from './extension-form-mqtt.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
var linker = function(scope, element) {
var template = $templateCache.get(extensionFormMqttTemplate);
element.html(template);
scope.types = types;
scope.theForm = scope.$parent.theForm;
scope.deviceNameExpressions = {
deviceNameJsonExpression: "extension.converter-json",
deviceNameTopicExpression: "extension.topic"
};
scope.deviceTypeExpressions = {
deviceTypeJsonExpression: "extension.converter-json",
deviceTypeTopicExpression: "extension.topic"
};
scope.attributeKeyExpressions = {
attributeKeyJsonExpression: "extension.converter-json",
attributeKeyTopicExpression: "extension.topic"
};
scope.requestIdExpressions = {
requestIdJsonExpression: "extension.converter-json",
requestIdTopicExpression: "extension.topic"
}
scope.extensionCustomConverterOptions = {
useWrapMode: false,
mode: 'json',
showGutter: true,
showPrintMargin: true,
theme: 'github',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function(_ace) {
_ace.$blockScrolling = 1;
}
};
scope.updateValidity = function () {
if(scope.brokers.length) {
for(let i=0;i<scope.brokers.length;i++) {
if(scope.brokers[i].credentials.type == scope.types.mqttCredentialTypes.pem.value) {
if(!(scope.brokers[i].credentials.caCert && scope.brokers[i].credentials.privateKey && scope.brokers[i].credentials.cert)) {
scope.theForm.$setValidity('cert.PEM', false);
break;
} else {
scope.theForm.$setValidity('cert.PEM', true);
}
}
}
}
};
scope.$watch('brokers', function() {
scope.updateValidity();
}, true);
scope.addBroker = function() {
var newBroker = {
host: "localhost",
port: 1882,
ssl: false,
retryInterval: 3000,
credentials: {type:"anonymous"},
mapping: [],
connectRequests: [],
disconnectRequests: [],
attributeRequests: [],
attributeUpdates: [],
serverSideRpc: []
};
scope.brokers.push(newBroker);
};
scope.removeBroker = function(broker) {
var index = scope.brokers.indexOf(broker);
if (index > -1) {
scope.brokers.splice(index, 1);
}
};
if(scope.isAdd) {
scope.brokers = [];
scope.config.brokers = scope.brokers;
scope.addBroker();
} else {
scope.brokers = scope.config.brokers;
}
scope.addMap = function(mapping) {
var newMap = {topicFilter:"sensors", converter:{attributes:[],timeseries:[]}};
mapping.push(newMap);
};
scope.removeMap = function(map, mapping) {
var index = mapping.indexOf(map);
if (index > -1) {
mapping.splice(index, 1);
}
};
scope.addAttribute = function(attributes) {
var newAttribute = {type:"", key:"", value:""};
attributes.push(newAttribute);
};
scope.removeAttribute = function(attribute, attributes) {
var index = attributes.indexOf(attribute);
if (index > -1) {
attributes.splice(index, 1);
}
};
scope.addConnectRequest = function(requests, type) {
var newRequest = {};
if(type == "connect") {
newRequest.topicFilter = "sensors/connect";
} else {
newRequest.topicFilter = "sensors/disconnect";
}
requests.push(newRequest);
};
scope.addAttributeRequest = function(requests) {
var newRequest = {
topicFilter: "sensors/attributes",
clientScope: false,
responseTopicExpression: "sensors/${deviceName}/attributes/${responseId}",
valueExpression: "${attributeValue}"
};
requests.push(newRequest);
};
scope.addAttributeUpdate = function(updates) {
var newUpdate = {
deviceNameFilter: ".*",
attributeFilter: ".*",
topicExpression: "sensor/${deviceName}/${attributeKey}",
valueExpression: "{\"${attributeKey}\":\"${attributeValue}\"}"
}
updates.push(newUpdate);
};
scope.addServerSideRpc = function(rpcRequests) {
var newRpc = {
deviceNameFilter: ".*",
methodFilter: "echo",
requestTopicExpression: "sensor/${deviceName}/request/${methodName}/${requestId}",
responseTopicExpression: "sensor/${deviceName}/response/${methodName}/${requestId}",
responseTimeout: 10000,
valueExpression: "${params}"
};
rpcRequests.push(newRpc);
};
scope.changeCredentials = function(broker) {
var type = broker.credentials.type;
broker.credentials = {};
broker.credentials.type = type;
};
scope.changeConverterType = function(map) {
if(map.converterType == "custom"){
map.converter = "";
}
if(map.converterType == "json") {
map.converter = {attributes:[],timeseries:[]};
}
};
scope.changeNameExpression = function(element, type) {
if(element.nameExp == "deviceNameJsonExpression") {
if(element.deviceNameTopicExpression) {
delete element.deviceNameTopicExpression;
}
if(type) {
element.deviceNameJsonExpression = "${$.serialNumber}";
}
}
if(element.nameExp == "deviceNameTopicExpression") {
if(element.deviceNameJsonExpression) {
delete element.deviceNameJsonExpression;
}
if(type && type == "connect") {
element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/connect)";
}
if(type && type == "disconnect") {
element.deviceNameTopicExpression = "(?<=sensor\\/)(.*?)(?=\\/disconnect)";
}
if(type && type == "attribute") {
element.deviceNameTopicExpression = "(?<=sensors\\/)(.*?)(?=\\/attributes)";
}
}
};
scope.changeTypeExpression = function(converter) {
if(converter.typeExp == "deviceTypeJsonExpression") {
if(converter.deviceTypeTopicExpression) {
delete converter.deviceTypeTopicExpression;
}
}
if(converter.typeExp == "deviceTypeTopicExpression") {
if(converter.deviceTypeJsonExpression) {
delete converter.deviceTypeJsonExpression;
}
}
};
scope.changeAttrKeyExpression = function(request) {
if(request.attrKey == "attributeKeyJsonExpression") {
if(request.attributeKeyTopicExpression) {
delete request.attributeKeyTopicExpression;
}
request.attributeKeyJsonExpression = "${$.key}";
}
if(request.attrKey == "attributeKeyTopicExpression") {
if(request.attributeKeyJsonExpression) {
delete request.attributeKeyJsonExpression;
}
request.attributeKeyTopicExpression = "(?<=attributes\\/)(.*?)(?=\\/request)";
}
};
scope.changeRequestIdExpression = function(request) {
if(request.requestId == "requestIdJsonExpression") {
if(request.requestIdTopicExpression) {
delete request.requestIdTopicExpression;
}
request.requestIdJsonExpression = "${$.requestId}";
}
if(request.requestId == "requestIdTopicExpression") {
if(request.requestIdJsonExpression) {
delete request.requestIdJsonExpression;
}
request.requestIdTopicExpression = "(?<=request\\/)(.*?)($)";
}
};
scope.validateCustomConverter = function(model, editorName) {
if(model && model.length) {
try {
angular.fromJson(model);
scope.theForm[editorName].$setValidity('converterJSON', true);
} catch(e) {
scope.theForm[editorName].$setValidity('converterJSON', false);
}
}
};
scope.fileAdded = function($file, broker, fileType) {
var reader = new FileReader();
reader.onload = function(event) {
scope.$apply(function() {
if(event.target.result) {
scope.theForm.$setDirty();
var addedFile = event.target.result;
if (addedFile && addedFile.length > 0) {
if(fileType == "caCert") {
broker.credentials.caCertFileName = $file.name;
broker.credentials.caCert = addedFile.replace(/^data.*base64,/, "");
}
if(fileType == "privateKey") {
broker.credentials.privateKeyFileName = $file.name;
broker.credentials.privateKey = addedFile.replace(/^data.*base64,/, "");
}
if(fileType == "Cert") {
broker.credentials.certFileName = $file.name;
broker.credentials.cert = addedFile.replace(/^data.*base64,/, "");
}
}
}
});
};
reader.readAsDataURL($file.file);
};
scope.clearFile = function(broker, fileType) {
scope.theForm.$setDirty();
if(fileType == "caCert") {
broker.credentials.caCertFileName = null;
broker.credentials.caCert = null;
}
if(fileType == "privateKey") {
broker.credentials.privateKeyFileName = null;
broker.credentials.privateKey = null;
}
if(fileType == "Cert") {
broker.credentials.certFileName = null;
broker.credentials.cert = null;
}
};
scope.collapseValidation = function(index, id) {
var invalidState = angular.element('#'+id+':has(.ng-invalid)');
if(invalidState.length) {
invalidState.addClass('inner-invalid');
}
};
scope.expandValidation = function (index, id) {
var invalidState = angular.element('#'+id);
invalidState.removeClass('inner-invalid');
};
$compile(element.contents())(scope);
};
return {
restrict: "A",
link: linker,
scope: {
config: "=",
isAdd: "="
}
}
}

860
ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html

@ -0,0 +1,860 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-card class="extension-form extension-mqtt">
<md-card-title name="testValid">
<md-card-title-text>
<span translate class="md-headline">extension.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<v-accordion id="mqtt-brokers-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-brokers-pane" expanded="true">
<v-pane-header>
{{ 'extension.brokers' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="brokers.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(brokerIndex,broker) in brokers">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeBroker(broker)" ng-hide="brokers.length < 2">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="40" class="md-block">
<label translate>extension.port</label>
<input required type="number" min="1" max="65535" name="mqttPort_{{brokerIndex}}" ng-model="broker.port">
<div ng-messages="theForm['mqttPort_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
<div translate ng-message="min">extension.port-range</div>
<div translate ng-message="max">extension.port-range</div>
</div>
</md-input-container>
<md-input-container flex="60" class="md-block">
<label translate>extension.host</label>
<input required name="mqttHost_{{brokerIndex}}" ng-model="broker.host">
<div ng-messages="theForm['mqttHost_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block">
<label translate>extension.retry-interval</label>
<input required type="number" name="mqttRetryInterval_{{brokerIndex}}" ng-model="broker.retryInterval">
<div ng-messages="theForm['mqttRetryInterval_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.credentials</label>
<md-select required name="mqttCredentials_{{brokerIndex}}" ng-model="broker.credentials.type" ng-change="changeCredentials(broker)">
<md-option ng-repeat="(credentialsType, credentialsValue) in types.mqttCredentialTypes" ng-value="credentialsValue.value">
{{credentialsValue.name | translate}}
</md-option>
</md-select>
</md-input-container>
<md-input-container flex="10" class="md-block">
<md-checkbox flex aria-label="{{ 'extension.ssl' | translate }}"
ng-model="broker.ssl">{{ 'extension.ssl' | translate }}
</md-checkbox>
</md-input-container>
</section>
<section flex layout="row" ng-if='broker.credentials.type == "basic"'>
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttUsername_' + brokerIndex].$touched && theForm['mqttUsername_' + brokerIndex].$invalid">
<label translate>extension.username</label>
<input required name="mqttUsername_{{brokerIndex}}" ng-model="broker.credentials.username">
<div ng-messages="theForm['mqttUsername_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="60" class="md-block" md-is-error="theForm['mqttPassword_' + brokerIndex].$touched && theForm['mqttPassword_' + brokerIndex].$invalid">
<label translate>extension.password</label>
<input required name="mqttPassword_{{brokerIndex}}" ng-model="broker.credentials.password">
<div ng-messages="theForm['mqttPassword_' + brokerIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="column" ng-if='broker.credentials.type == "cert.PEM"' class="dropdown-section">
<div class="tb-container" ng-class="broker.credentials.caCertFileName ? 'ng-valid' : 'ng-invalid'">
<label class="tb-label" translate>extension.ca-cert</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "caCert")' class="tb-file-select-container">
<div class="tb-file-clear-container">
<md-button ng-click='clearFile(broker, "caCert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
</md-button>
</div>
<div class="alert tb-flow-drop" flow-drop>
<label for="caCertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
<input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="caCertSelect_{{brokerIndex}}">
</div>
</div>
</div>
<div class="dropdown-messages">
<div ng-if="!broker.credentials.caCertFileName" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="broker.credentials.caCertFileName">{{broker.credentials.caCertFileName}}</div>
</div>
<div class="tb-container" ng-class="broker.credentials.privateKeyFileName ? 'ng-valid' : 'ng-invalid'">
<label class="tb-label" translate>extension.private-key</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "privateKey")' class="tb-file-select-container">
<div class="tb-file-clear-container">
<md-button ng-click='clearFile(broker, "privateKey")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
</md-button>
</div>
<div class="alert tb-flow-drop" flow-drop>
<label for="privateKeySelect_{{brokerIndex}}" translate>extension.drop-file</label>
<input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="privateKeySelect_{{brokerIndex}}">
</div>
</div>
</div>
<div class="dropdown-messages">
<div ng-if="!broker.credentials.privateKeyFileName" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="broker.credentials.privateKeyFileName">{{broker.credentials.privateKeyFileName}}</div>
</div>
<div class="tb-container" ng-class="broker.credentials.certFileName ? 'ng-valid' : 'ng-invalid'">
<label class="tb-label" translate>extension.cert</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, broker, "Cert")' class="tb-file-select-container">
<div class="tb-file-clear-container">
<md-button ng-click='clearFile(broker, "Cert")' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
</md-button>
</div>
<div class="alert tb-flow-drop" flow-drop>
<label for="CertSelect_{{brokerIndex}}" translate>extension.drop-file</label>
<input class="file-input" flow-btn flow-attrs="{accept:'.pem'}" id="CertSelect_{{brokerIndex}}">
</div>
</div>
</div>
<div class="dropdown-messages">
<div ng-if="!broker.credentials.certFileName" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="broker.credentials.certFileName">{{broker.credentials.certFileName}}</div>
</div>
</section>
<v-accordion id="mqtt-mapping-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-mapping-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.mapping' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.mapping.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(mapIndex,map) in broker.mapping">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeMap(map, broker.mapping)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttConverterType_' + brokerIndex + mapIndex].$touched && theForm['mqttConverterType_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.converter-type</label>
<md-select required name="mqttConverterType_{{brokerIndex}}{{mapIndex}}" ng-model="map.converterType" ng-change="changeConverterType(map)">
<md-option ng-repeat="(converterType, value) in types.mqttConverterTypes" ng-value="converterType">
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttConverterType_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="60" class="md-block">
<label translate>extension.topic-filter</label>
<input required name="mqttTopicFilter_{{brokerIndex}}{{mapIndex}}" ng-model="map.topicFilter">
<div ng-messages="theForm['mqttTopicFilter_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<div ng-if='map.converterType =="json"' ng-init="map.converter.type = 'json'">
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.device-name-expression</label>
<md-select required name="mqttDeviceNameExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.nameExp" ng-change="changeNameExpression(map.converter)">
<md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttDeviceNameExpression_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="map.converter.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block" md-is-error="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$touched && theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.json-name-expression</label>
<input required name="mqttJsonNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameJsonExpression">
<div ng-messages="theForm['mqttJsonNameExp_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="map.converter.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block" md-is-error="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$touched && theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.topic-name-expression</label>
<input required name="mqttTopicNameExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceNameTopicExpression">
<div ng-messages="theForm['mqttTopicNameExp_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$touched && theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.device-type-expression</label>
<md-select required name="mqttDeviceTypeExpression_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.typeExp" ng-change="changeTypeExpression(map.converter)">
<md-option ng-repeat="(key, value) in deviceTypeExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttDeviceTypeExpression_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="map.converter.typeExp == 'deviceTypeJsonExpression'" flex="60" class="md-block" md-is-error="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.json-type-expression</label>
<input required name="mqttJsonTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeJsonExpression">
<div ng-messages="theForm['mqttJsonTypeExp_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="map.converter.typeExp == 'deviceTypeTopicExpression'" flex="60" class="md-block" md-is-error="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$touched && theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$invalid">
<label translate>extension.topic-type-expression</label>
<input required name="mqttTopicTypeExp_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.deviceTypeTopicExpression">
<div ng-messages="theForm['mqttTopicTypeExp_' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block">
<label translate>extension.timeout</label>
<input type="number" name="mqttTimeout_{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.timeout" parse-to-null>
</md-input-container>
<md-input-container flex="60" class="md-block" md-is-error="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$touched && theForm['mqttFilterExpression' + brokerIndex + mapIndex].$invalid">
<label translate>extension.filter-expression</label>
<input required name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
<div ng-messages="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
</div>
<div ng-if='map.converterType == "custom"'>
<div class="md-caption" style="padding-left: 3px; padding-bottom: 10px; color: rgba(0,0,0,0.57);" translate>extension.transformer-json</div>
<div flex class="tb-extension-custom-transformer-panel">
<div flex class="tb-extension-custom-transformer"
ui-ace="extensionCustomConverterOptions"
ng-model="map.converter"
name="mqttCustomConverter_{{brokerIndex}}{{mapIndex}}"
ng-change='validateCustomConverter(map.converter, "mqttCustomConverter_" + brokerIndex + mapIndex)'
required>
</div>
</div>
<div class="tb-error-messages" ng-messages="theForm['mqttCustomConverter_' + brokerIndex + mapIndex].$error" role="alert">
<div ng-message="required" class="tb-error-message" translate>extension.converter-json-required</div>
<div ng-message="converterJSON" class="tb-error-message" translate>extension.converter-json-parse</div>
</div>
</div>
<v-accordion ng-if='map.converterType =="json"' id="mqtt-attributes-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-attributes-pane_{{brokerIndex}}{{mapIndex}}">
<v-pane-header>
{{ 'extension.attributes' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="map.converter.attributes.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(attributeIndex, attribute) in map.converter.attributes">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attribute, map.converter.attributes)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$invalid">
<label translate>extension.key</label>
<input required name="mqttAttributeKey_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.key">
<div ng-messages="theForm['mqttAttributeKey_' + brokerIndex + mapIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$invalid">
<label translate>extension.type</label>
<md-select required name="mqttAttributeType_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.type">
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block" md-is-error="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$touched && theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$invalid">
<label translate>extension.value</label>
<input required name="mqttAttributeValue_{{brokerIndex}}{{mapIndex}}{{attributeIndex}}" ng-model="attribute.value">
<div ng-messages="theForm['mqttAttributeValue_' + brokerIndex + mapIndex + attributeIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttribute(map.converter.attributes)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion ng-if='map.converterType =="json"' id="mqtt-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-timeseries-pane_{{brokerIndex}}{{mapIndex}}">
<v-pane-header>
{{ 'extension.timeseries' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="map.converter.timeseries.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(timeseriesIndex, timeseries) in map.converter.timeseries">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(timeseries, map.converter.timeseries)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block" md-is-error="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
<label translate>extension.key</label>
<input required name="mqttTimeseriesKey_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.key">
<div ng-messages="theForm['mqttTimeseriesKey_' + brokerIndex + mapIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block" md-is-error="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
<label translate>extension.type</label>
<md-select required name="mqttTimeseriesType_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.type">
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType" ng-value="attrType">
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block" md-is-error="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$touched && theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$invalid">
<label translate>extension.value</label>
<input required name="mqttTimeseriesValue_{{brokerIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
<div ng-messages="theForm['mqttTimeseriesValue_' + brokerIndex + mapIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttribute(map.converter.timeseries)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addMap(broker.mapping)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-map</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-connect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-connect-requests-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.connect-requests' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.connectRequests.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(connectRequestIndex, connectRequest) in broker.connectRequests">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(connectRequest, broker.connectRequests)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<md-input-container class="md-block">
<label translate>extension.topic-filter</label>
<input required name="conRequestTopicFilter_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.topicFilter">
<div ng-messages="theForm['conRequestTopicFilter_' + brokerIndex + connectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$touched && theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$invalid">
<label translate>extension.device-name-expression</label>
<md-select required name="connectDeviceNameExpression_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.nameExp" ng-change="changeNameExpression(connectRequest, 'connect')">
<md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['connectDeviceNameExpression_' + brokerIndex + connectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="connectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
<label translate>extension.json-name-expression</label>
<input required name="connectJsonNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameJsonExpression">
<div ng-messages="theForm['connectJsonNameExp_' + brokerIndex + connectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="connectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
<label translate>extension.topic-name-expression</label>
<input required name="connectTopicNameExp_{{brokerIndex}}{{connectRequestIndex}}" ng-model="connectRequest.deviceNameTopicExpression">
<div ng-messages="theForm['connectTopicNameExp_' + brokerIndex + connectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click='addConnectRequest(broker.connectRequests, "connect")' aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-connect-request</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-disconnect-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-disconnect-requests-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.disconnect-requests' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.disconnectRequests.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(disconnectRequestIndex, disconnectRequest) in broker.disconnectRequests">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(disconnectRequest, broker.disconnectRequests)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<md-input-container class="md-block">
<label translate>extension.topic-filter</label>
<input required name="disconRequestTopicFilter_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.topicFilter">
<div ng-messages="theForm['disconRequestTopicFilter_' + brokerIndex + disconnectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$touched && theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$invalid">
<label translate>extension.device-name-expression</label>
<md-select required name="disconnectDeviceNameExpression_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.nameExp" ng-change="changeNameExpression(disconnectRequest, 'disconnect')">
<md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['disconnectDeviceNameExpression_' + brokerIndex + disconnectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
<label translate>extension.json-name-expression</label>
<input required name="disconnectJsonNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameJsonExpression">
<div ng-messages="theForm['disconnectJsonNameExp_' + brokerIndex + disconnectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="disconnectRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
<label translate>extension.topic-name-expression</label>
<input required name="disconnectTopicNameExp_{{brokerIndex}}{{disconnectRequestIndex}}" ng-model="disconnectRequest.deviceNameTopicExpression">
<div ng-messages="theForm['disconnectTopicNameExp_' + brokerIndex + disconnectRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click='addConnectRequest(broker.disconnectRequests, "disconnect")' aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-disconnect-request</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-attribute-requests-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-attribute-requests-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.attribute-requests' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.attributeRequests.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(attributeRequestIndex, attributeRequest) in broker.attributeRequests">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeRequest, broker.attributeRequests)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="80" class="md-block">
<label translate>extension.topic-filter</label>
<input required name="attributeRequestTopicFilter_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.topicFilter">
<div ng-messages="theForm['attributeRequestTopicFilter_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="20" class="md-block">
<md-checkbox flex aria-label="{{ 'extension.client-scope' | translate }}"
ng-model="attributeRequest.clientScope">{{ 'extension.client-scope' | translate }}
</md-checkbox>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$invalid">
<label translate>extension.device-name-expression</label>
<md-select required name="attrRequestDeviceNameExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.nameExp" ng-change="changeNameExpression(attributeRequest, 'attribute')">
<md-option ng-repeat="(key, value) in deviceNameExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['attrRequestDeviceNameExpression_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.nameExp == 'deviceNameJsonExpression'" flex="60" class="md-block">
<label translate>extension.json-name-expression</label>
<input required name="attrRequestJsonNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameJsonExpression">
<div ng-messages="theForm['attrRequestJsonNameExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.nameExp == 'deviceNameTopicExpression'" flex="60" class="md-block">
<label translate>extension.topic-name-expression</label>
<input required name="attrRequestTopicNameExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.deviceNameTopicExpression">
<div ng-messages="theForm['attrRequestTopicNameExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$invalid">
<label translate>extension.attribute-key-expression</label>
<md-select required name="attrRequestAttributeKeyExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attrKey" ng-change="changeAttrKeyExpression(attributeRequest)">
<md-option ng-repeat="(key, value) in attributeKeyExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['attrRequestAttributeKeyExpression_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyJsonExpression'" flex="60" class="md-block">
<label translate>extension.attr-json-key-expression</label>
<input required name="attrRequestJsonKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyJsonExpression">
<div ng-messages="theForm['attrRequestJsonKeyExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.attrKey == 'attributeKeyTopicExpression'" flex="60" class="md-block">
<label translate>extension.attr-topic-key-expression</label>
<input required name="attrRequestTopicKeyExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.attributeKeyTopicExpression">
<div ng-messages="theForm['attrRequestTopicKeyExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="40" class="md-block" md-is-error="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$touched && theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$invalid">
<label translate>extension.request-id-expression</label>
<md-select required name="attrRequestIdExpression_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestId" ng-change="changeRequestIdExpression(attributeRequest)">
<md-option ng-repeat="(key, value) in requestIdExpressions" ng-value='key'>
{{value | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['attrRequestIdExpression_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.requestId == 'requestIdJsonExpression'" flex="60" class="md-block">
<label translate>extension.request-id-json-expression</label>
<input required name="attrRequestJsonIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdJsonExpression">
<div ng-messages="theForm['attrRequestJsonIdExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container ng-if="attributeRequest.requestId == 'requestIdTopicExpression'" flex="60" class="md-block">
<label translate>extension.request-id-topic-expression</label>
<input required name="attrRequestTopicIdExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.requestIdTopicExpression">
<div ng-messages="theForm['attrRequestTopicIdExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block">
<label translate>extension.response-topic-expression</label>
<input required name="attributeRequestResponseTopicExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.responseTopicExpression">
<div ng-messages="theForm['attributeRequestResponseTopicExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>extension.value-expression</label>
<input required name="attributeRequestValueExp_{{brokerIndex}}{{attributeRequestIndex}}" ng-model="attributeRequest.valueExpression">
<div ng-messages="theForm['attributeRequestValueExp_' + brokerIndex + attributeRequestIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addAttributeRequest(broker.attributeRequests)" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute-request</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-attribute-updates-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-attribute-updates-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.attribute-updates' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.attributeUpdates.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(attributeUpdateIndex, attributeUpdate) in broker.attributeUpdates">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(attributeUpdate, broker.attributeUpdates)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.device-name-filter</label>
<input required name="attributeUpdateDeviceNameFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.deviceNameFilter">
<div ng-messages="theForm['attributeUpdateDeviceNameFilter_' + brokerIndex + attributeUpdateIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.attribute-filter</label>
<input required name="attributeUpdateAttributeFilter_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.attributeFilter">
<div ng-messages="theForm['attributeUpdateAttributeFilter_' + brokerIndex + attributeUpdateIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block">
<label translate>extension.topic-expression</label>
<input required name="attributeUpdateTopicExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.topicExpression">
<div ng-messages="theForm['attributeUpdateTopicExp_' + brokerIndex + attributeUpdateIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>extension.value-expression</label>
<input required name="attributeUpdateValueExp_{{brokerIndex}}{{attributeUpdateIndex}}" ng-model="attributeUpdate.valueExpression">
<div ng-messages="theForm['attributeUpdateValueExp_' + brokerIndex + attributeUpdateIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click='addAttributeUpdate(broker.attributeUpdates)' aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute-update</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="mqtt-server-side-rpc-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="mqtt-server-side-rpc-pane_{{brokerIndex}}">
<v-pane-header>
{{ 'extension.server-side-rpc' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="broker.serverSideRpc.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(rpcIndex, rpc) in broker.serverSideRpc">
<md-button aria-label="{{ 'action.remove' | translate }}" class="md-icon-button" ng-click="removeAttribute(rpc, broker.serverSideRpc)">
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.device-name-filter</label>
<input required name="serverSideRpcDeviceNameFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.deviceNameFilter">
<div ng-messages="theForm['serverSideRpcDeviceNameFilter_' + brokerIndex + rpcIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.method-filter</label>
<input required name="serverSideRpcMethodFilter_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.methodFilter">
<div ng-messages="theForm['serverSideRpcMethodFilter_' + brokerIndex + rpcIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
<md-input-container class="md-block">
<label translate>extension.request-topic-expression</label>
<input required name="serverSideRpcRequestTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.requestTopicExpression">
<div ng-messages="theForm['serverSideRpcRequestTopicExp_' + brokerIndex + rpcIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>extension.response-topic-expression</label>
<input name="serverSideRpcResponseTopicExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTopicExpression" parse-to-null>
</md-input-container>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.response-timeout</label>
<input type="number" name="serverSideRpcResponseTimeout_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.responseTimeout" parse-to-null>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.value-expression</label>
<input required name="serverSideRpcValueExp_{{brokerIndex}}{{rpcIndex}}" ng-model="rpc.valueExpression">
<div ng-messages="theForm['serverSideRpcValueExp_' + brokerIndex + rpcIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click='addServerSideRpc(broker.serverSideRpc)' aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-server-side-rpc-request</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addBroker()" aria-label="{{ 'action.add' | translate }}">
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-broker</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<!--<pre>
{{config | json}}
</pre>-->
</md-card-content>
</md-card>

173
ui/src/app/extension/extensions-forms/extension-form-opc.directive.js

@ -0,0 +1,173 @@
/*
* 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 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/theme/github';
import './extension-form.scss';
/* eslint-disable angular/log */
import extensionFormOpcTemplate from './extension-form-opc.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ExtensionFormOpcDirective($compile, $templateCache, $translate, types) {
var linker = function(scope, element) {
function Server() {
this.applicationName = "Thingsboard OPC-UA client";
this.applicationUri = "";
this.host = "localhost";
this.port = 49320;
this.scanPeriodInSeconds = 10;
this.timeoutInMillis = 5000;
this.security = "Basic128Rsa15";
this.identity = {
"type": "anonymous"
};
this.keystore = {
"type": "PKCS12",
"location": "example.pfx",
"password": "secret",
"alias": "gateway",
"keyPassword": "secret"
};
this.mapping = []
}
function Map() {
this.deviceNodePattern = "Channel1\\.Device\\d+$";
this.deviceNamePattern = "Device ${_System._DeviceId}";
this.attributes = [];
this.timeseries = [];
}
function Attribute() {
this.key = "Tag1";
this.type = "string";
this.value = "${Tag1}";
}
function Timeseries() {
this.key = "Tag2";
this.type = "long";
this.value = "${Tag2}";
}
var template = $templateCache.get(extensionFormOpcTemplate);
element.html(template);
scope.types = types;
scope.theForm = scope.$parent.theForm;
if (!scope.configuration.servers.length) {
scope.configuration.servers.push(new Server());
}
scope.addServer = function(serversList) {
serversList.push(new Server());
// scope.addMap(serversList[serversList.length-1].mapping);
scope.theForm.$setDirty();
};
scope.addMap = function(mappingList) {
mappingList.push(new Map());
scope.theForm.$setDirty();
};
scope.addNewAttribute = function(attributesList) {
attributesList.push(new Attribute());
scope.theForm.$setDirty();
};
scope.addNewTimeseries = function(timeseriesList) {
timeseriesList.push(new Timeseries());
scope.theForm.$setDirty();
};
scope.removeItem = (item, itemList) => {
var index = itemList.indexOf(item);
if (index > -1) {
itemList.splice(index, 1);
}
scope.theForm.$setDirty();
};
$compile(element.contents())(scope);
scope.fileAdded = function($file, model, options) {
let reader = new FileReader();
reader.onload = function(event) {
scope.$apply(function() {
if(event.target.result) {
scope.theForm.$setDirty();
let addedFile = event.target.result;
if (addedFile && addedFile.length > 0) {
model[options.fileName] = $file.name;
model[options.file] = addedFile.replace(/^data.*base64,/, "");
}
}
});
};
reader.readAsDataURL($file.file);
};
scope.clearFile = function(model, options) {
scope.theForm.$setDirty();
model[options.fileName] = null;
model[options.file] = null;
};
scope.collapseValidation = function(index, id) {
var invalidState = angular.element('#'+id+':has(.ng-invalid)');
if(invalidState.length) {
invalidState.addClass('inner-invalid');
}
};
scope.expandValidation = function (index, id) {
var invalidState = angular.element('#'+id);
invalidState.removeClass('inner-invalid');
};
};
return {
restrict: "A",
link: linker,
scope: {
configuration: "=",
isAdd: "="
}
}
}

557
ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html

@ -0,0 +1,557 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-card class="extension-form extension-opc">
<md-card-title>
<md-card-title-text>
<span translate class="md-headline">extension.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<v-accordion id="opc-server-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-servers-pane" expanded="true">
<v-pane-header>
{{ 'extension.opc-server' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="configuration.servers.length === 0">
<span translate layout-align="center center" class="tb-prompt">extension.opc-add-server-prompt</span>
</div>
<div ng-if="configuration.servers.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(serverIndex, server) in configuration.servers">
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(server, configuration.servers)"
ng-hide="configuration.servers.length < 2"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<div layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-application-name</label>
<input required name="applicationName_{{serverIndex}}" ng-model="server.applicationName">
<div ng-messages="theForm['applicationName_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block" md-is-error="theForm['applicationUri_' + serverIndex].$touched && theForm['applicationUri_' + serverIndex].$invalid">
<label translate>extension.opc-application-uri</label>
<input required name="applicationUri_{{serverIndex}}" ng-model="server.applicationUri">
<div ng-messages="theForm['applicationUri_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.host</label>
<input required name="host_{{serverIndex}}" ng-model="server.host">
<div ng-messages="theForm['host_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.port</label>
<input type="number"
required
name="port_{{serverIndex}}"
ng-model="server.port"
min="1"
max="65535"
>
<div ng-messages="theForm['port_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>Port should be in a range from 1 to 65535</div>
<div translate
ng-message="max"
>Port should be in a range from 1 to 65535</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-scan-period-in-seconds</label>
<input type="number"
required
name="scanPeriodInSeconds_{{serverIndex}}"
ng-model="server.scanPeriodInSeconds">
<div ng-messages="theForm['scanPeriodInSeconds_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.timeout</label>
<input type="number"
required name="timeoutInMillis_{{serverIndex}}"
ng-model="server.timeoutInMillis"
>
<div ng-messages="theForm['timeoutInMillis_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.opc-security</label>
<md-select required
name="securityType_{{serverIndex}}"
ng-model="server.security">
<md-option ng-value="securityType"
ng-repeat="(securityType, securityValue) in types.extensionOpcSecurityTypes"
><span ng-bind="::securityValue"></span></md-option>
</md-select>
<div ng-messages="theForm['securityType_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.opc-identity</label>
<md-select required
name="identityType_{{serverIndex}}"
ng-model="server.identity.type"
>
<md-option ng-value="identityType"
ng-repeat="(identityType, identityValue) in types.extensionIdentityType"
><span ng-bind="identityValue | translate"></span></md-option>
</md-select>
<div ng-messages="theForm['identityType_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div ng-if="server.identity.type != 'username'">
<span class=""
ng-init="server.identity = {'type':'anonymous'}"></span>
</div>
<div layout="row" ng-if="server.identity.type == 'username'">
<md-input-container flex="50" class="md-block" md-is-error="theForm['identityUsername_' + serverIndex].$touched && theForm['identityUsername_' + serverIndex].$invalid">
<label translate>extension.username</label>
<input required
name="identityUsername_{{serverIndex}}"
ng-model="server.identity.username"
>
<div ng-messages="theForm['identityUsername_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block" md-is-error="theForm['identityPassword_' + serverIndex].$touched && theForm['identityPassword_' + serverIndex].$invalid">
<label translate>extension.password</label>
<input required
name="identityPassword_{{serverIndex}}" ng-model="server.identity.password">
<div ng-messages="theForm['identityPassword_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<v-accordion id="opc-keystore-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-keystore-pane__{{serverIndex}}" expanded="true">
<v-pane-header>
{{ 'extension.opc-keystore' | translate }}
</v-pane-header>
<v-pane-content>
<md-input-container class="md-block tb-container-for-select">
<label translate>extension.opc-keystore-type</label>
<md-select required name="keystoreType_{{serverIndex}}" ng-model="server.keystore.type">
<md-option ng-value="keystoreType" ng-repeat="(keystoreType, keystoreValue) in types.extensionKeystoreType"><span ng-bind="::keystoreValue"></span></md-option>
</md-select>
<div ng-messages="theForm['keystoreType_'+serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<section class="dropdown-section">
<div class="tb-container" ng-class="{'ng-invalid':!server.keystore.file}">
<span ng-init='fieldsToFill = {"fileName":"fileName", "file":"file"}'></span>
<label class="tb-label" translate>extension.opc-keystore-location</label>
<div flow-init="{singleFile:true}" flow-file-added='fileAdded($file, server.keystore, fieldsToFill)' class="tb-file-select-container">
<div class="tb-file-clear-container">
<md-button ng-click='clearFile(server.keystore, fieldsToFill)' class="tb-file-clear-btn md-icon-button md-primary" aria-label="{{ 'action.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
<md-icon aria-label="{{ 'action.remove' | translate }}" class="material-icons">close</md-icon>
</md-button>
</div>
<div class="alert tb-flow-drop" flow-drop>
<label for="dropFileKeystore_{{serverIndex}}" translate>extension.drop-file</label>
<input flow-attrs="{accept:'.pfx,.p12'}"
type="file"
class="file-input"
flow-btn id="dropFileKeystore_{{serverIndex}}"
name="keystoreFile"
ng-model="server.keystore.file"
>
</div>
</div>
</div>
<div class="dropdown-messages">
<div ng-if="!server.keystore[fieldsToFill.fileName]" class="tb-error-message" translate>extension.no-file</div>
<div ng-if="server.keystore[fieldsToFill.fileName]">{{server.keystore[fieldsToFill.fileName]}}</div>
</div>
</section>
<div flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-keystore-password</label>
<input required name="keystorePassword_{{serverIndex}}" ng-model="server.keystore.password">
<div ng-messages="theForm['keystorePassword_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-keystore-alias</label>
<input required name="keystoreAlias_{{serverIndex}}" ng-model="server.keystore.alias">
<div ng-messages="theForm['keystoreAlias_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</div>
<md-input-container class="md-block">
<label translate>extension.opc-keystore-key-password</label>
<input required name="keystoreKeyPassword_{{serverIndex}}" ng-model="server.keystore.keyPassword">
<div ng-messages="theForm['keystoreKeyPassword_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="opc-mapping-accordion"
class="vAccordion--default"
onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-mapping-pane_{{serverIndex}}">
<v-pane-header>
{{ 'extension.mapping' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="server.mapping.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(mapIndex, map) in server.mapping"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(map, server.mapping)"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<div flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-device-node-pattern</label>
<input required
name="deviceNodePattern_{{serverIndex}}{{mapIndex}}"
ng-model="map.deviceNodePattern"
>
<div ng-messages="theForm['deviceNodePattern_' + serverIndex + mapIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.opc-device-name-pattern</label>
<input required
name="deviceNamePattern_{{serverIndex}}{{mapIndex}}"
ng-model="map.deviceNamePattern"
>
<div ng-messages="theForm['deviceNamePattern_' + serverIndex + mapIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<v-accordion id="opc-attributes-accordion"
class="vAccordion--default"
onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-attributes-pane_{{serverIndex}}{{mapIndex}}">
<v-pane-header>
{{ 'extension.attributes' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-show="map.attributes.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(attributeIndex, attribute) in map.attributes"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(attribute, map.attributes)">
<ng-md-icon icon="close"
aria-label="{{ 'action.remove' | translate }}"
></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex
layout="row"
>
<md-input-container flex="60" class="md-block">
<label translate>extension.key</label>
<input required
name="opcAttributeKey_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
ng-model="attribute.key"
>
<div ng-messages="theForm['opcAttributeKey_' + serverIndex + mapIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="opcAttributeType_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
ng-model="attribute.type"
>
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
ng-value="attrType"
>
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['opcAttributeType_' + serverIndex + mapIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.required-type</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="100" class="md-block">
<label translate>extension.value</label>
<input required name="opcAttributeValue_{{serverIndex}}{{mapIndex}}{{attributeIndex}}"
ng-model="attribute.value"
>
<div ng-messages="theForm['opcAttributeValue_' + serverIndex + mapIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addNewAttribute(map.attributes)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>add-attribute</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="opc-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-timeseries-pane_{{serverIndex}}{{mapIndex}}">
<v-pane-header>
{{ 'extension.timeseries' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-show="map.timeseries.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(timeseriesIndex, timeseries) in map.timeseries"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(timeseries, map.timeseries)"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="60" class="md-block">
<label translate>extension.key</label>
<input required
name="opcTimeseriesKey_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
ng-model="timeseries.key"
>
<div ng-messages="theForm['opcTimeseriesKey_' + serverIndex + mapIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="40"
class="md-block tb-container-for-select"
>
<label translate>extension.type</label>
<md-select required
name="opcTimeseriesType_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}"
ng-model="timeseries.type"
>
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
ng-value="attrType"
>
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['opcTimeseriesType_' + serverIndex + mapIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="100" class="md-block">
<label translate>extension.value</label>
<input required name="opcTimeseriesValue_{{serverIndex}}{{mapIndex}}{{timeseriesIndex}}" ng-model="timeseries.value">
<div ng-messages="theForm['opcTimeseriesValue_' + serverIndex + mapIndex + timeseriesIndex].$error">
<div translate ng-message="required">extension.required-value</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addNewAttribute(map.timeseries)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex
layout="row"
layout-align="start center"
>
<md-button class="md-primary md-raised"
ng-click="addMap(server.mapping)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-map</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
<div flex
layout="row"
layout-align="start center"
>
<md-button class="md-primary md-raised"
ng-click="addServer(configuration.servers)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.opc-add-server</span>
</md-button>
</div>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<!--{{config}}-->
</md-card-content>
</md-card>

76
ui/src/app/extension/extensions-forms/extension-form.scss

@ -0,0 +1,76 @@
/**
* 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.
*/
.extension-form {
li > .md-button {
color: rgba(0, 0, 0, 0.7);
margin: 0;
}
.vAccordion--default {
margin-top: 0;
padding-left: 3px;
}
.tb-container {
width:100%;
}
.dropdown-messages {
.tb-error-message {
padding: 5px 0 0 0;
}
}
.dropdown-section {
margin-bottom: 30px;
}
v-pane.inner-invalid > v-pane-header {
border-bottom: 2px solid rgb(221,44,0);
}
}
.extension-form.extension-mqtt {
md-checkbox{
margin-left: 10px;
}
}
.tb-extension-custom-transformer-panel {
margin-left: 15px;
border: 1px solid #C0C0C0;
height: 100%;
.tb-extension-custom-transformer {
min-width: 600px;
min-height: 200px;
width: 100%;
height: 100%;
}
.ace_text-input {
position:absolute!important
}
}
.extensionDialog {
min-width: 1000px;
}
.tb-container-for-select {
height: 58px;
}
.tb-drop-file-input-hide {
height: 200%;
display: block;
position: absolute;
bottom: 0;
width: 100%;
}

29
ui/src/app/extension/index.js

@ -0,0 +1,29 @@
/*
* 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 ExtensionTableDirective from './extension-table.directive';
import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive';
import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive'
import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive';
import {ParseToNull} from './extension-dialog.controller';
export default angular.module('thingsboard.extension', [])
.directive('tbExtensionTable', ExtensionTableDirective)
.directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
.directive('tbExtensionFormMqtt', ExtensionFormMqttDirective)
.directive('tbExtensionFormOpc', ExtensionFormOpcDirective)
.directive('parseToNull', ParseToNull)
.name;

90
ui/src/app/import-export/import-export.service.js

@ -24,8 +24,9 @@ import entityAliasesTemplate from '../entity/alias/entity-aliases.tpl.html';
/* eslint-disable no-undef, angular/window-service, angular/document-service */
/*@ngInject*/
export default function ImportExport($log, $translate, $q, $mdDialog, $document, itembuffer, utils, types, dashboardUtils,
entityService, dashboardService, pluginService, ruleService, widgetService, toast) {
export default function ImportExport($log, $translate, $q, $mdDialog, $document, $http, itembuffer, utils, types,
dashboardUtils, entityService, dashboardService, pluginService, ruleService,
widgetService, toast, attributeService) {
var service = {
@ -40,8 +41,11 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
exportWidgetType: exportWidgetType,
importWidgetType: importWidgetType,
exportWidgetsBundle: exportWidgetsBundle,
importWidgetsBundle: importWidgetsBundle
}
importWidgetsBundle: importWidgetsBundle,
exportExtension: exportExtension,
importExtension: importExtension,
exportToPc: exportToPc
};
return service;
@ -614,6 +618,84 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return true;
}
function exportExtension(extensionId) {
getExtension(extensionId)
.then(
function success(extension) {
var name = extension.title;
name = name.toLowerCase().replace(/\W/g,"_");
exportToPc(prepareExport(extension), name + '.json');
},
function fail(rejection) {
var message = rejection;
if (!message) {
message = $translate.instant('error.unknown-error');
}
toast.showError($translate.instant('extension.export-failed-error', {error: message}));
}
);
function getExtension(extensionId) {
var deferred = $q.defer();
var url = '/api/plugins/telemetry/DEVICE/' + extensionId;
$http.get(url, null)
.then(function success(response) {
deferred.resolve(response.data);
}, function fail() {
deferred.reject();
});
return deferred.promise;
}
}
function importExtension($event, options) {
var deferred = $q.defer();
openImportDialog($event, 'extension.import-extensions', 'extension.file')
.then(
function success(extension) {
if (!validateImportedExtension(extension)) {
toast.showError($translate.instant('extension.invalid-file-error'));
deferred.reject();
} else {
attributeService
.saveEntityAttributes(
options.entityType,
options.entityId,
types.attributesScope.shared.value,
[{
key: "configuration",
value: angular.toJson(extension)
}]
)
.then(function success() {
options.successFunc();
});
}
},
function fail() {
deferred.reject();
}
);
return deferred.promise;
}
function validateImportedExtension(configuration) {
if (configuration.length) {
for (let i = 0; i < configuration.length; i++) {
if (angular.isUndefined(configuration[i].configuration) || angular.isUndefined(configuration[i].id )|| angular.isUndefined(configuration[i].type)) {
return false;
}
}
} else {
return false;
}
return true;
}
function processEntityAliases(entityAliases, aliasIds) {
var deferred = $q.defer();
var missingEntityAliases = {};

2
ui/src/app/layout/index.js

@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive';
import thingsboardEntity from '../entity';
import thingsboardEvent from '../event';
import thingsboardAlarm from '../alarm';
import thingsboardExtension from '../extension';
import thingsboardTenant from '../tenant';
import thingsboardCustomer from '../customer';
import thingsboardUser from '../user';
@ -66,6 +67,7 @@ export default angular.module('thingsboard.home', [
thingsboardEntity,
thingsboardEvent,
thingsboardAlarm,
thingsboardExtension,
thingsboardTenant,
thingsboardCustomer,
thingsboardUser,

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

@ -729,6 +729,133 @@ export default angular.module('thingsboard.locale', [])
"messages-processed": "Messages processed",
"errors-occurred": "Errors occurred"
},
"extension": {
"extensions": "Extensions",
"selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected",
"type": "Type",
"key": "Key",
"value": "Value",
"id": "Id",
"extension-id": "Extension id",
"extension-type": "Extension type",
"transformer-json": "JSON *",
"unique-id-required": "Current extension id already exists.",
"delete": "Delete extension",
"add": "Add extension",
"edit": "Edit extension",
"delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
"delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
"delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?",
"delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
"converters": "Converters",
"converter-id": "Converter id",
"configuration": "Configuration",
"converter-configurations": "Converter configurations",
"token": "Security token",
"add-converter": "Add converter",
"add-config": "Add converter configuration",
"device-name-expression": "Device name expression",
"device-type-expression": "Device type expression",
"custom": "Custom",
"to-double": "To Double",
"transformer": "Transformer",
"json-required": "Transformer json is required.",
"json-parse": "Unable to parse transformer json.",
"attributes": "Attributes",
"add-attribute": "Add attribute",
"add-map": "Add mapping element",
"timeseries": "Timeseries",
"add-timeseries": "Add timeseries",
"field-required": "Field is required",
"brokers": "Brokers",
"add-broker": "Add broker",
"host": "Host",
"port": "Port",
"port-range": "Port should be in a range from 1 to 65535.",
"ssl": "Ssl",
"credentials": "Credentials",
"username": "Username",
"password": "Password",
"retry-interval": "Retry interval in milliseconds",
"anonymous": "Anonymous",
"basic": "Basic",
"pem": "PEM",
"ca-cert": "CA certificate file *",
"private-key": "Private key file *",
"cert": "Certificate file *",
"no-file": "No file selected.",
"drop-file": "Drop a file or click to select a file to upload.",
"mapping": "Mapping",
"topic-filter": "Topic filter",
"converter-type": "Converter type",
"converter-json": "Json",
"json-name-expression": "Device name json expression",
"topic-name-expression": "Device name topic expression",
"json-type-expression": "Device type json expression",
"topic-type-expression": "Device type topic expression",
"attribute-key-expression": "Attribute key expression",
"attr-json-key-expression": "Attribute key json expression",
"attr-topic-key-expression": "Attribute key topic expression",
"request-id-expression": "Request id expression",
"request-id-json-expression": "Request id json expression",
"request-id-topic-expression": "Request id topic expression",
"response-topic-expression": "Response topic expression",
"value-expression": "Value expression",
"topic": "Topic",
"timeout": "Timeout in milliseconds",
"converter-json-required": "Converter json is required.",
"converter-json-parse": "Unable to parse converter json.",
"filter-expression": "Filter expression",
"connect-requests": "Connect requests",
"add-connect-request": "Add connect request",
"disconnect-requests": "Disconnect requests",
"add-disconnect-request": "Add disconnect request",
"attribute-requests": "Attribute requests",
"add-attribute-request": "Add attribute request",
"attribute-updates": "Attribute updates",
"add-attribute-update": "Add attribute update",
"server-side-rpc": "Server side RPC",
"add-server-side-rpc-request": "Add server-side RPC request",
"device-name-filter": "Device name filter",
"attribute-filter": "Attribute filter",
"method-filter": "Method filter",
"request-topic-expression": "Request topic expression",
"response-timeout": "Response timeout in milliseconds",
"topic-expression": "Topic expression",
"client-scope": "Client scope",
"opc-server": "Servers",
"opc-add-server": "Add server",
"opc-application-name": "Application name",
"opc-application-uri": "Application uri",
"opc-scan-period-in-seconds": "Scan period in seconds",
"opc-security": "Security",
"opc-identity": "Identity",
"opc-keystore": "Keystore",
"opc-type": "Type",
"opc-keystore-type":"Type",
"opc-keystore-location":"Location *",
"opc-keystore-password":"Password",
"opc-keystore-alias":"Alias",
"opc-keystore-key-password":"Key password",
"opc-device-node-pattern":"Device node pattern",
"opc-device-name-pattern":"Device name pattern",
"sync": {
"status": "Status",
"sync": "Sync",
"not-sync": "Not sync",
"last-sync-time": "Last sync time",
"not-available": "Not available"
},
"export-extensions-configuration":"Export extensions configuration",
"import-extensions-configuration":"Import extensions configuration",
"import-extensions": "Import extensions",
"import-extension": "Import extension",
"export-extension": "Export extension",
"file": "Extensions file",
"invalid-file-error": "Invalid extension file"
},
"fullscreen": {
"expand": "Expand to fullscreen",
"exit": "Exit fullscreen",
@ -851,7 +978,6 @@ export default angular.module('thingsboard.locale', [])
"invalid-plugin-file-error": "Unable to import plugin: Invalid plugin data structure.",
"copyId": "Copy plugin Id",
"idCopiedMessage": "Plugin Id has been copied to clipboard"
},
"position": {
"top": "Top",
@ -1071,7 +1197,8 @@ export default angular.module('thingsboard.locale', [])
"boolean": "Boolean",
"boolean-value": "Boolean value",
"false": "False",
"true": "True"
"true": "True",
"long": "Long"
},
"widget": {
"widget-library": "Widgets Library",

135
ui/src/app/widget/lib/extensions-table-widget.js

@ -0,0 +1,135 @@
/*
* 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 './extensions-table-widget.scss';
/* eslint-disable import/no-unresolved, import/default */
import extensionsTableWidgetTemplate from './extensions-table-widget.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
export default angular.module('thingsboard.widgets.extensionsTableWidget', [])
.directive('tbExtensionsTableWidget', ExtensionsTableWidget)
.name;
/*@ngInject*/
function ExtensionsTableWidget() {
return {
restrict: "E",
scope: true,
bindToController: {
ctx: '='
},
controller: ExtensionsTableWidgetController,
controllerAs: 'vm',
templateUrl: extensionsTableWidgetTemplate
};
}
/*@ngInject*/
function ExtensionsTableWidgetController($scope, $translate, utils) {
var vm = this;
vm.datasources = null;
vm.tabsHidden = false;
$scope.$watch('vm.ctx', function() {
if (vm.ctx && vm.ctx.defaultSubscription) {
vm.settings = vm.ctx.settings;
vm.subscription = vm.ctx.defaultSubscription;
vm.datasources = vm.subscription.datasources;
initializeConfig();
updateDatasources();
}
});
function initializeConfig() {
if (vm.settings.extensionsTitle && vm.settings.extensionsTitle.length) {
vm.extensionsTitle = utils.customTranslation(vm.settings.extensionsTitle, vm.settings.extensionsTitle);
} else {
vm.extensionsTitle = $translate.instant('extension.extensions');
}
vm.ctx.widgetTitle = vm.extensionsTitle;
vm.ctx.widgetActions = [vm.importExtensionsAction, vm.exportExtensionsAction, vm.addAction, vm.searchAction, vm.refreshAction];
}
function updateDatasources() {
var datasource = vm.datasources[0];
vm.selectedSource = vm.datasources[0];
vm.ctx.widgetTitle = utils.createLabelFromDatasource(datasource, vm.extensionsTitle);
}
vm.changeSelectedSource = function(source) {
vm.selectedSource = source;
};
vm.searchAction = {
name: "action.search",
show: true,
onAction: function() {
$scope.$broadcast("showSearch", vm.selectedSource);
},
icon: "search"
};
vm.refreshAction = {
name: "action.refresh",
show: true,
onAction: function() {
$scope.$broadcast("refreshExtensions", vm.selectedSource);
},
icon: "refresh"
};
vm.addAction = {
name: "action.add",
show: true,
onAction: function() {
$scope.$broadcast("addExtension", vm.selectedSource);
},
icon: "add"
};
vm.exportExtensionsAction = {
name: "extension.export-extensions-configuration",
show: true,
onAction: function() {
$scope.$broadcast("exportExtensions", vm.selectedSource);
},
icon: "file_download"
};
vm.importExtensionsAction = {
name: "extension.import-extensions-configuration",
show: true,
onAction: function() {
$scope.$broadcast("importExtensions", vm.selectedSource);
},
icon: "file_upload"
};
$scope.$on("filterMode", function($event, mode) {
vm.tabsHidden = mode;
});
$scope.$on("selectedExtensions", function($event, mode) {
vm.tabsHidden = mode;
});
}

29
ui/src/app/widget/lib/extensions-table-widget.scss

@ -0,0 +1,29 @@
/**
* 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-extension-table {
md-content {
background-color: #fff;
}
}
md-tabs.hide-tabs-menu {
md-tabs-wrapper {
display: none;
}
md-tabs-content-wrapper {
top: 0 !important;
}
}

27
ui/src/app/widget/lib/extensions-table-widget.tpl.html

@ -0,0 +1,27 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-tabs id="tabs" md-border-bottom flex class="tb-absolute-fill" ng-class="{'hide-tabs-menu': vm.datasources.length == 1 || vm.tabsHidden}">
<md-tab ng-repeat="source in vm.datasources" label="{{ source.name }}" md-on-select="vm.changeSelectedSource(source)">
<tb-extension-table flex
entity-id="source.entityId"
entity-type="{{source.entityType}}"
in-widget="true"
ctx="vm.ctx">
</tb-extension-table>
</md-tab>
</md-tabs>
Loading…
Cancel
Save