thingsboard-developers
Changes
ui/package.json 1(+1 -0)
ui/src/app/app.js 3(+3 -0)
ui/src/app/common/types.constant.js 14(+14 -0)
ui/src/app/extension/extension-dialog.controller.js 157(+129 -28)
ui/src/app/extension/extension-dialog.tpl.html 39(+22 -17)
ui/src/app/extension/index.js 2(+2 -0)
ui/src/app/locale/locale.constant.js 82(+49 -33)
Details
ui/package.json 1(+1 -0)
diff --git a/ui/package.json b/ui/package.json
index cb8fdbd..81f4574 100644
--- a/ui/package.json
+++ b/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",
ui/src/app/app.js 3(+3 -0)
diff --git a/ui/src/app/app.js b/ui/src/app/app.js
index 4a6f3f0..5b19089 100644
--- a/ui/src/app/app.js
+++ b/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',
ui/src/app/common/types.constant.js 14(+14 -0)
diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js
index f78e0a8..5b3c9e4 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -350,6 +350,20 @@ export default angular.module('thingsboard.types', [])
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",
ui/src/app/extension/extension-dialog.controller.js 157(+129 -28)
diff --git a/ui/src/app/extension/extension-dialog.controller.js b/ui/src/app/extension/extension-dialog.controller.js
index e580929..fe5825c 100644
--- a/ui/src/app/extension/extension-dialog.controller.js
+++ b/ui/src/app/extension/extension-dialog.controller.js
@@ -29,45 +29,88 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
vm.entityId = entityId;
vm.allExtensions = allExtensions;
- vm.configuration = {};
- vm.newExtension = {id:"",type:"",configuration:vm.configuration};
- if(!vm.isAdd) {
- vm.newExtension = angular.copy(extension);
- vm.configuration = vm.newExtension.configuration;
- editTransformers(vm.newExtension);
+ if (extension) {
+ vm.extension = angular.copy(extension);
+ editTransformers(vm.extension);
+ } else {
+ vm.extension = {};
}
- vm.cancel = cancel;
- vm.save = save;
+ 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() {
- $mdDialog.hide();
- saveTransformers();
- if(vm.isAdd) {
- vm.allExtensions.push(vm.newExtension);
+ 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 {
- var index = vm.allExtensions.indexOf(extension);
- if(index > -1) {
- vm.allExtensions[index] = vm.newExtension;
+
+ if(vm.isAdd) {
+ vm.allExtensions.push(vm.extension);
+ } else {
+ var index = vm.allExtensions.indexOf(extension);
+ if(index > -1) {
+ vm.allExtensions[index] = vm.extension;
+ }
}
- }
- var editedValue = angular.toJson(vm.allExtensions);
+ $mdDialog.hide();
+ saveTransformers();
- attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
- function success() {
- $scope.theForm.$setPristine();
- }
- );
+ 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.newExtension.id;
+ return ext.id == vm.extension.id;
});
if(coincidenceArray.length) {
if(!vm.isAdd) {
@@ -82,11 +125,11 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
}
- }
+ };
function saveTransformers() {
- if(vm.newExtension.type == types.extensionType.http) {
- var config = vm.newExtension.configuration.converterConfigurations;
+ 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++){
@@ -106,8 +149,8 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
}
}
}
- if(vm.newExtension.type == types.extensionType.mqtt) {
- var brokers = vm.newExtension.configuration.brokers;
+ 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) {
@@ -119,6 +162,27 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
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;
+ }
+ }
}
}
}
@@ -174,6 +238,43 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
}
}
}
+ 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";
+ }
+ }
+ }
}
}
}
ui/src/app/extension/extension-dialog.tpl.html 39(+22 -17)
diff --git a/ui/src/app/extension/extension-dialog.tpl.html b/ui/src/app/extension/extension-dialog.tpl.html
index e3f3663..73c33d8 100644
--- a/ui/src/app/extension/extension-dialog.tpl.html
+++ b/ui/src/app/extension/extension-dialog.tpl.html
@@ -15,8 +15,8 @@
limitations under the License.
-->
-<md-dialog aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}" style="min-width: 1000px;">
- <form name="theForm" ng-submit="vm.save()">
+<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>
@@ -26,49 +26,54 @@
</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-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.newExtension.id" ng-change="vm.validateId()">
+ <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.id-required</div>
+ <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-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-model="vm.newExtension.type">
+
+ <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.type-required</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
-
- <div tb-extension-form-http config="vm.configuration" is-add="vm.isAdd" ng-if="vm.newExtension.type && vm.newExtension.type == vm.types.extensionType.http"></div>
- <div tb-extension-form-mqtt config="vm.configuration" is-add="vm.isAdd" ng-if="vm.newExtension.type && vm.newExtension.type == vm.types.extensionType.mqtt"></div>
-
+ <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>
-
- <!--<div>{{vm.newExtension}}</div>-->
</md-content>
</div>
</md-dialog-content>
+
<md-dialog-actions layout="row">
- <span flex></span>
- <md-button ng-disabled="loading || theForm.$invalid || !theForm.$dirty" type="submit"
- class="md-raised md-primary">
+ <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>
diff --git a/ui/src/app/extension/extensions-forms/extension-form.scss b/ui/src/app/extension/extensions-forms/extension-form.scss
index f40970b..cc3e052 100644
--- a/ui/src/app/extension/extensions-forms/extension-form.scss
+++ b/ui/src/app/extension/extensions-forms/extension-form.scss
@@ -22,9 +22,6 @@
margin-top: 0;
padding-left: 3px;
}
- .t-right {
- text-align: right;
- }
.tb-container {
width:100%;
}
@@ -33,6 +30,15 @@
padding: 5px 0 0 0;
}
}
+ .dropdown-section {
+ margin-bottom: 30px;
+ }
+}
+
+.extension-form.extension-mqtt {
+ md-checkbox{
+ margin-left: 10px;
+ }
}
.tb-extension-custom-transformer-panel {
@@ -48,4 +54,20 @@
.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%;
}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
index b185b4b..9484ab4 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
+++ b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js
@@ -53,74 +53,62 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
}
};
- if(scope.isAdd) {
- scope.converterConfigs = [];
- scope.config.converterConfigurations = scope.converterConfigs;
- } else {
- scope.converterConfigs = scope.config.converterConfigurations;
- }
-
- scope.updateValidity = function() {
- var valid = scope.converterConfigs && scope.converterConfigs.length > 0;
- scope.theForm.$setValidity('converterConfigs', valid);
- if(scope.converterConfigs.length) {
- for(let i=0;i<scope.converterConfigs.length;i++) {
- if(!scope.converterConfigs[i].converters.length) {
- scope.theForm.$setValidity('converters', false);
- break;
- } else {
- scope.theForm.$setValidity('converters', true);
- }
- }
- }
- };
-
- scope.$watch('converterConfigs', function() {
- scope.updateValidity();
- }, true);
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.theForm.$setDirty();
- }
+ };
scope.addConverter = function(converters) {
- var newConverter = {deviceNameJsonExpression:"", deviceTypeJsonExpression:"", attributes:[], timeseries:[]};
+ 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.theForm.$setDirty();
- }
+ };
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.theForm.$setDirty();
+ };
+
+
+ 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) {
@@ -131,10 +119,10 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
scope.theForm[editorName].$setValidity('transformerJSON', false);
}
}
- }
-
+ };
+
$compile(element.contents())(scope);
- }
+ };
return {
restrict: "A",
diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
index eb3bb3e..8202d29 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
+++ b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
@@ -23,18 +23,19 @@
</md-card-title>
<md-card-content>
<v-accordion id="http-converter-configs-accordion" class="vAccordion--default">
- <v-pane id="http-converters-pane" expanded="isAdd">
+ <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">
- <span translate layout-align="center center" class="tb-prompt">extension.add-config-prompt</span>
- </div>
<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)">
+ <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 }}
@@ -43,11 +44,11 @@
<md-card>
<md-card-content>
- <md-input-container class="md-block">
+ <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.converter-id-required</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
@@ -55,18 +56,21 @@
<input name="httpToken" ng-model="config.token" parse-to-null>
</md-input-container>
<v-accordion id="http-converters-accordion" class="vAccordion--default">
- <v-pane id="http-converters-pane">
+ <v-pane id="http-converters-pane" expanded="true">
<v-pane-header>
{{ 'extension.converters' | translate }}
</v-pane-header>
<v-pane-content>
- <div ng-if="config.converters.length === 0">
- <span translate layout-align="center center" class="tb-prompt">extension.add-converter-prompt</span>
- </div>
<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)">
+ <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 }}
@@ -74,18 +78,18 @@
</md-button>
<md-card>
<md-card-content>
- <md-input-container class="md-block">
+ <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.device-name-expression-required</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
- <md-input-container class="md-block">
+ <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.device-type-expression-required</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
@@ -107,14 +111,14 @@
<md-card>
<md-card-content>
<section flex layout="row">
- <md-input-container flex="60" class="md-block">
+ <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.required-key</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
- <md-input-container flex="40" class="md-block">
+ <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">
@@ -122,16 +126,16 @@
</md-option>
</md-select>
<div ng-messages="theForm['httpAttributeType_' + configIndex + converterIndex + attributeIndex].$error">
- <div translate ng-message="required">extension.required-type</div>
+ <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-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.required-value</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
@@ -172,11 +176,8 @@
<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-tooltip md-direction="top">
- {{ 'extension.add-attribute' | translate }}
- </md-tooltip>
<md-icon class="material-icons">add</md-icon>
- <span translate>action.add</span>
+ <span translate>extension.add-attribute</span>
</md-button>
</div>
</v-pane-content>
@@ -202,14 +203,14 @@
<md-card>
<md-card-content>
<section flex layout="row">
- <md-input-container flex="60" class="md-block">
+ <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.required-key</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
- <md-input-container flex="40" class="md-block">
+ <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">
@@ -217,16 +218,16 @@
</md-option>
</md-select>
<div ng-messages="theForm['httpTimeseriesType_' + configIndex + converterIndex + timeseriesIndex].$error">
- <div translate ng-message="required">extension.required-type</div>
+ <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-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.required-value</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
@@ -267,11 +268,8 @@
<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-tooltip md-direction="top">
- {{ 'extension.add-timeseries' | translate }}
- </md-tooltip>
<md-icon class="material-icons">add</md-icon>
- <span translate>action.add</span>
+ <span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
@@ -285,11 +283,8 @@
<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-tooltip md-direction="top">
- {{ 'extension.add-converter' | translate }}
- </md-tooltip>
<md-icon class="material-icons">add</md-icon>
- <span translate>action.add</span>
+ <span translate>extension.add-converter</span>
</md-button>
</div>
</v-pane-content>
@@ -304,11 +299,8 @@
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addConverterConfig()" aria-label="{{ 'action.add' | translate }}">
- <md-tooltip md-direction="top">
- {{ 'extension.add-config' | translate }}
- </md-tooltip>
<md-icon class="material-icons">add</md-icon>
- <span translate>action.add</span>
+ <span translate>extension.add-config</span>
</md-button>
</div>
</v-pane-content>
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
index 4101bb5..41e9aad 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.directive.js
@@ -33,14 +33,22 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
scope.types = types;
scope.theForm = scope.$parent.theForm;
- scope.nameExpressions = {
+ scope.deviceNameExpressions = {
deviceNameJsonExpression: "extension.converter-json",
deviceNameTopicExpression: "extension.topic"
};
- scope.typeExpressions = {
+ 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,
@@ -58,17 +66,7 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
}
};
-
- if(scope.isAdd) {
- scope.brokers = [];
- scope.config.brokers = scope.brokers;
- } else {
- scope.brokers = scope.config.brokers;
- }
-
scope.updateValidity = function () {
- var valid = scope.brokers && scope.brokers.length > 0;
- scope.theForm.$setValidity('brokers', valid);
if(scope.brokers.length) {
for(let i=0;i<scope.brokers.length;i++) {
if(scope.brokers[i].credentials.type == scope.types.mqttCredentialTypes.pem.value) {
@@ -81,57 +79,116 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
}
}
}
- }
+ };
scope.$watch('brokers', function() {
scope.updateValidity();
}, true);
scope.addBroker = function() {
- var newBroker = {host:"localhost", port:1882, ssl:false, retryInterval:3000, credentials:{type:"anonymous"}, mapping:[]};
+ 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);
}
- scope.theForm.$setDirty();
+ };
+
+ 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.theForm.$setDirty();
- }
+ };
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.theForm.$setDirty();
- }
+ };
+
+ 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"){
@@ -140,20 +197,32 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
if(map.converterType == "json") {
map.converter = {attributes:[],timeseries:[]};
}
- }
+ };
- scope.changeNameExpression = function(converter) {
- if(converter.nameExp == "deviceNameJsonExpression") {
- if(converter.deviceNameTopicExpression) {
- delete converter.deviceNameTopicExpression;
+ scope.changeNameExpression = function(element, type) {
+ if(element.nameExp == "deviceNameJsonExpression") {
+ if(element.deviceNameTopicExpression) {
+ delete element.deviceNameTopicExpression;
+ }
+ if(type) {
+ element.deviceNameJsonExpression = "${$.serialNumber}";
}
}
- if(converter.nameExp == "deviceNameTopicExpression") {
- if(converter.deviceNameJsonExpression) {
- delete converter.deviceNameJsonExpression;
+ 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") {
@@ -166,7 +235,37 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
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) {
@@ -177,7 +276,7 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
scope.theForm[editorName].$setValidity('converterJSON', false);
}
}
- }
+ };
scope.fileAdded = function($file, broker, fileType) {
var reader = new FileReader();
@@ -204,7 +303,7 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
});
};
reader.readAsDataURL($file.file);
- }
+ };
scope.clearFile = function(broker, fileType) {
scope.theForm.$setDirty();
@@ -220,10 +319,10 @@ export default function ExtensionFormHttpDirective($compile, $templateCache, $tr
broker.credentials.certFileName = null;
broker.credentials.cert = null;
}
- }
+ };
$compile(element.contents())(scope);
- }
+ };
return {
restrict: "A",
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
index 94ae789..7950c75 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
+++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
@@ -23,18 +23,15 @@
</md-card-title>
<md-card-content>
<v-accordion id="mqtt-brokers-accordion" class="vAccordion--default">
- <v-pane id="mqtt-brokers-pane" expanded="isAdd">
+ <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">
- <span translate layout-align="center center" class="tb-prompt">extension.add-broker-prompt</span>
- </div>
<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)">
+ <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 }}
@@ -47,7 +44,7 @@
<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.port-required</div>
+ <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>
@@ -56,7 +53,7 @@
<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.host-required</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
@@ -65,7 +62,7 @@
<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.retry-interval-required</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
@@ -76,30 +73,30 @@
</md-option>
</md-select>
</md-input-container>
- <md-input-container flex="10" class="md-block t-right">
+ <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-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.username-required</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
- <md-input-container flex="60" class="md-block">
+ <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.password-required</div>
+ <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"'>
- <div class="tb-container">
+ <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">
@@ -120,7 +117,7 @@
<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">
+ <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">
@@ -182,7 +179,7 @@
<md-card>
<md-card-content>
<section flex layout="row">
- <md-input-container flex="40" class="md-block">
+ <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">
@@ -190,64 +187,70 @@
</md-option>
</md-select>
<div ng-messages="theForm['mqttConverterType_' + brokerIndex + mapIndex].$error">
- <div translate ng-message="required">extension.converter-type-required</div>
+ <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.topic-filter-required</div>
+ <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">
- <label translate>extension.name-expression</label>
+ <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 nameExpressions" ng-value='key'>
+ <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-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.json-name-expression-required</div>
+ <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-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.topic-name-expression-required</div>
+ <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.type-expression</label>
+ <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 typeExpressions" ng-value='key'>
+ <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-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.json-type-expression-required</div>
+ <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-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.topic-type-expression-required</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
@@ -256,11 +259,11 @@
<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-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.filter-expression-required</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
@@ -301,14 +304,14 @@
<md-card>
<md-card-content>
<section flex layout="row">
- <md-input-container flex="60" class="md-block">
+ <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.required-key</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
- <md-input-container flex="40" class="md-block">
+ <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">
@@ -316,15 +319,15 @@
</md-option>
</md-select>
<div ng-messages="theForm['mqttAttributeType_' + brokerIndex + mapIndex + attributeIndex].$error">
- <div translate ng-message="required">extension.required-type</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
- <md-input-container class="md-block">
+ <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.required-value</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
@@ -335,11 +338,8 @@
<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-tooltip md-direction="top">
- {{ 'extension.add-attribute' | translate }}
- </md-tooltip>
<md-icon class="material-icons">add</md-icon>
- <span translate>action.add</span>
+ <span translate>extension.add-attribute</span>
</md-button>
</div>
</v-pane-content>
@@ -364,14 +364,14 @@
<md-card>
<md-card-content>
<section flex layout="row">
- <md-input-container flex="60" class="md-block">
+ <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.required-key</div>
+ <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-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">
@@ -379,15 +379,15 @@
</md-option>
</md-select>
<div ng-messages="theForm['mqttTimeseriesType_' + brokerIndex + mapIndex + timeseriesIndex].$error">
- <div translate ng-message="required">extension.required-type</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</section>
- <md-input-container class="md-block">
+ <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.required-value</div>
+ <div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
</md-card-content>
@@ -398,11 +398,8 @@
<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-tooltip md-direction="top">
- {{ 'extension.add-timeseries' | translate }}
- </md-tooltip>
<md-icon class="material-icons">add</md-icon>
- <span translate>action.add</span>
+ <span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
@@ -417,16 +414,429 @@
<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-tooltip md-direction="top">
- {{ 'extension.add-map' | translate }}
- </md-tooltip>
<md-icon class="material-icons">add</md-icon>
- <span translate>action.add</span>
+ <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">
+ <v-pane id="mqtt-connect-requests-pane">
+ <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">
+ <v-pane id="mqtt-disconnect-requests-pane">
+ <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">
+ <v-pane id="mqtt-attribute-requests-pane">
+ <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">
+ <v-pane id="mqtt-attribute-updates-pane">
+ <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">
+ <v-pane id="mqtt-server-side-rpc-pane">
+ <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>
@@ -436,18 +846,15 @@
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addBroker()" aria-label="{{ 'action.add' | translate }}">
- <md-tooltip md-direction="top">
- {{ 'extension.add-broker' | translate }}
- </md-tooltip>
<md-icon class="material-icons">add</md-icon>
- <span translate>action.add</span>
+ <span translate>extension.add-broker</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
-<pre>
+<!--<pre>
{{config | json}}
-</pre>
+</pre>-->
</md-card-content>
</md-card>
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
new file mode 100644
index 0000000..7eeeb29
--- /dev/null
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
@@ -0,0 +1,161 @@
+/*
+ * 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;
+
+ };
+
+ };
+
+ return {
+ restrict: "A",
+ link: linker,
+ scope: {
+ configuration: "=",
+ isAdd: "="
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
index 779e93a..3cc1e1b 100644
--- a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
+++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
@@ -15,4 +15,543 @@
limitations under the License.
-->
-<div>OPC UA</div>
\ No newline at end of file
+<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="http-server-configs-accordion" class="vAccordion--default">
+ <v-pane id="http-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-attributes-accordion" class="vAccordion--default">
+ <v-pane id="opc-attributes-pane" 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-attributes-accordion"
+ class="vAccordion--default"
+ >
+ <v-pane id="opc-attributes-pane">
+ <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"
+ >
+ <v-pane id="opc-attributes-pane">
+ <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">
+ <v-pane id="opc-timeseries-pane">
+ <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>
\ No newline at end of file
diff --git a/ui/src/app/extension/extension-table.directive.js b/ui/src/app/extension/extension-table.directive.js
index ae28d24..21d716b 100644
--- a/ui/src/app/extension/extension-table.directive.js
+++ b/ui/src/app/extension/extension-table.directive.js
@@ -126,11 +126,13 @@ function ExtensionTableController($scope, $filter, $document, $translate, types,
controllerAs: 'vm',
templateUrl: extensionDialogTemplate,
parent: angular.element($document[0].body),
- locals: { isAdd: isAdd,
- allExtensions: vm.allExtensions,
- entityId: vm.entityId,
- entityType: vm.entityType,
- extension: extension},
+ locals: {
+ isAdd: isAdd,
+ allExtensions: vm.allExtensions,
+ entityId: vm.entityId,
+ entityType: vm.entityType,
+ extension: extension
+ },
bindToController: true,
targetEvent: $event,
fullscreen: true,
ui/src/app/extension/index.js 2(+2 -0)
diff --git a/ui/src/app/extension/index.js b/ui/src/app/extension/index.js
index 619374f..acb97be 100644
--- a/ui/src/app/extension/index.js
+++ b/ui/src/app/extension/index.js
@@ -17,11 +17,13 @@
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;
\ No newline at end of file
ui/src/app/locale/locale.constant.js 82(+49 -33)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 3ac0ebc..8551a38 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -739,12 +739,7 @@ export default angular.module('thingsboard.locale', [])
"extension-id": "Extension id",
"extension-type": "Extension type",
"transformer-json": "JSON *",
- "id-required": "Extension id is required.",
"unique-id-required": "Current extension id already exists.",
- "type-required": "Extension type is required.",
- "required-type": "Type is required.",
- "required-key": "Key is required.",
- "required-value": "Value is required.",
"delete": "Delete extension",
"add": "Add extension",
"edit": "Edit extension",
@@ -754,18 +749,13 @@ export default angular.module('thingsboard.locale', [])
"delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
"converters": "Converters",
"converter-id": "Converter id",
- "converter-id-required": "Converter id is required.",
"configuration": "Configuration",
"converter-configurations": "Converter configurations",
"token": "Security token",
"add-converter": "Add converter",
- "add-converter-prompt": "Please add converter",
"add-config": "Add converter configuration",
- "add-config-prompt": "Please add converter configuration",
"device-name-expression": "Device name expression",
- "device-name-expression-required": "Device name expression is required.",
"device-type-expression": "Device type expression",
- "device-type-expression-required": "Device type expression is required.",
"custom": "Custom",
"to-double": "To Double",
"transformer": "Transformer",
@@ -773,25 +763,20 @@ export default angular.module('thingsboard.locale', [])
"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",
- "add-broker-prompt": "Please add broker",
"host": "Host",
- "host-required": "Host is required.",
"port": "Port",
- "port-required": "Port is required.",
"port-range": "Port should be in a range from 1 to 65535.",
"ssl": "Ssl",
"credentials": "Credentials",
"username": "Username",
- "username-required": "Username is required.",
"password": "Password",
- "password-required": "Password is required.",
- "retry-interval": "Retry interval",
- "retry-interval-required": "Retry interval is required.",
+ "retry-interval": "Retry interval in milliseconds",
"anonymous": "Anonymous",
"basic": "Basic",
"pem": "PEM",
@@ -801,28 +786,59 @@ export default angular.module('thingsboard.locale', [])
"no-file": "No file selected.",
"drop-file": "Drop a file or click to select a file to upload.",
"mapping": "Mapping",
- "add-map": "Add map",
"topic-filter": "Topic filter",
- "topic-filter-required": "Topic filter is required.",
"converter-type": "Converter type",
- "converter-type-required": "Converter type is required.",
"converter-json": "Json",
- "name-expression": "Name expression",
- "type-expression": "Type expression",
- "json-name-expression": "Json name expression",
- "json-name-expression-required": "Json name expression is required.",
- "topic-name-expression": "Topic name expression",
- "topic-name-expression-required": "Topic name expression is required.",
- "json-type-expression": "Json type expression",
- "json-type-expression-required": "Json type expression is required.",
- "topic-type-expression": "Topic type expression",
- "topic-type-expression-required": "Topic type expression is required.",
+ "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",
+ "timeout": "Timeout in milliseconds",
"converter-json-required": "Converter json is required.",
"converter-json-parse": "Unable to parse converter json.",
"filter-expression": "Filter expression",
- "filter-expression-required": "Filter expression is required."
+ "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",
},
"fullscreen": {
"expand": "Expand to fullscreen",