thingsboard-developers
Changes
ui/src/app/common/types.constant.js 14(+14 -0)
ui/src/app/extension/index.js 2(+2 -0)
ui/src/app/locale/locale.constant.js 32(+31 -1)
Details
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 dff7635..52c08d2 100644
--- a/ui/src/app/common/types.constant.js
+++ b/ui/src/app/common/types.constant.js
@@ -332,6 +332,20 @@ export default angular.module('thingsboard.types', [])
toDouble: 'extension.to-double',
custom: 'extension.custom'
},
+ extensionOpcSecurityTypes: {
+ Basic128Rsa15: "Basic128Rsa15",
+ Basic256: "Basic256",
+ Basic256Sha256: "Basic256Sha256",
+ None: "None"
+ },
+ extensionIdentityType: {
+ anonymous: "anonymous",
+ username: "username"
+ },
+ extensionKeystoreType: {
+ PKCS12: "PKCS12",
+ JKS: "JKS"
+ },
latestTelemetry: {
value: "LATEST_TELEMETRY",
name: "attribute.scope-latest-telemetry",
diff --git a/ui/src/app/extension/extension-dialog.controller.js b/ui/src/app/extension/extension-dialog.controller.js
index 3b13214..cc8d7c3 100644
--- a/ui/src/app/extension/extension-dialog.controller.js
+++ b/ui/src/app/extension/extension-dialog.controller.js
@@ -29,45 +29,72 @@ 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) { // Editing
+ //vm.configuration = vm.extension.configuration;
+ vm.extension = angular.copy(extension);
+ editTransformers(vm.extension);
+ } else { // Add new
+ vm.extension = {};
}
- vm.cancel = cancel;
- vm.save = save;
+ vm.extensionTypeChange = function () {
+ // $scope.theForm.$setPristine();
+ // $scope.theForm.$setUntouched();
+
+ 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() {
saveTransformers();
if(vm.isAdd) {
- vm.allExtensions.push(vm.newExtension);
+ vm.allExtensions.push(vm.extension);
} else {
var index = vm.allExtensions.indexOf(extension);
if(index > -1) {
- vm.allExtensions[index] = vm.newExtension;
+ vm.allExtensions[index] = vm.extension;
}
}
var editedValue = angular.toJson(vm.allExtensions);
- attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
- function success() {
- $scope.theForm.$setPristine();
- $mdDialog.hide();
- }
- );
+ attributeService
+ .saveEntityAttributes(
+ vm.entityType,
+ vm.entityId,
+ types.attributesScope.shared.value,
+ [{key:"configuration", value:editedValue}]
+ )
+ .then(function success() {
+ $scope.theForm.$setPristine();
+ $mdDialog.hide();
+ });
}
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 +109,11 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
}
- }
+ };
function saveTransformers() {
- var config = vm.newExtension.configuration.converterConfigurations;
- if(vm.newExtension.type == types.extensionType.http) {
+ var config = vm.extension.configuration.converterConfigurations;
+ if(vm.extension.type == types.extensionType.http) {
for(let i=0;i<config.length;i++) {
for(let j=0;j<config[i].converters.length;j++){
for(let k=0;k<config[i].converters[j].attributes.length;k++){
diff --git a/ui/src/app/extension/extension-dialog.tpl.html b/ui/src/app/extension/extension-dialog.tpl.html
index edb6918..b4fac57 100644
--- a/ui/src/app/extension/extension-dialog.tpl.html
+++ b/ui/src/app/extension/extension-dialog.tpl.html
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<md-dialog aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}" style="min-width: 1000px;">
+<md-dialog class="extensionDialog" aria-label="{{ (vm.isAdd ? 'extension.add' : 'extension.edit' ) | translate }}">
<form name="theForm" ng-submit="vm.save()">
<md-toolbar>
<div class="md-toolbar-tools">
@@ -26,8 +26,11 @@
</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">
@@ -35,41 +38,48 @@
<section flex layout="row">
<md-input-container flex="60" class="md-block">
<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="uniqueIdValidation">extension.unique-id-required</div>
</div>
</md-input-container>
+
<md-input-container flex="40" class="md-block">
<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>
</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-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-opc configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
</fieldset>
-
- <!--<div>{{vm.newExtension}}</div>-->
+ <!--<div>{{vm.extension}}</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"
+ <md-button type="submit"
+ ng-disabled="loading || theForm.$invalid || !theForm.$dirty"
class="md-raised md-primary">
{{ (vm.isAdd ? 'action.add' : 'action.save') | translate }}
</md-button>
+
<md-button ng-disabled="loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' | translate }}
</md-button>
</md-dialog-actions>
</form>
-</md-dialog>
\ No newline at end of file
+</md-dialog>
+
diff --git a/ui/src/app/extension/extensions-forms/extension-form.scss b/ui/src/app/extension/extensions-forms/extension-form.scss
index cfea19f..142516c 100644
--- a/ui/src/app/extension/extensions-forms/extension-form.scss
+++ b/ui/src/app/extension/extensions-forms/extension-form.scss
@@ -37,4 +37,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-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..e19984e 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,552 @@
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.opc-field-required</div>
+ </div>
+ </md-input-container>
+
+
+ <md-input-container flex="50" class="md-block">
+ <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.opc-field-required</div>
+ </div>
+ </md-input-container>
+ </div>
+
+
+ <div layout="row">
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-host</label>
+ <input required name="host_{{serverIndex}}" ng-model="server.host">
+ <div ng-messages="theForm['host_' + serverIndex].$error">
+ <div translate ng-message="required">extension.opc-field-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-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.opc-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.opc-field-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-timeout-in-millis</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.opc-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.opc-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"></span></md-option>
+ </md-select>
+ <div ng-messages="theForm['identityType_' + serverIndex].$error">
+ <div translate
+ ng-message="required"
+ >extension.opc-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">
+ <label translate>extension.opc-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.opc-field-required</div>
+ </div>
+ </md-input-container>
+
+ <md-input-container flex="50" class="md-block">
+ <label translate>extension.opc-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.opc-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.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.opc-field-required</div>
+ </div>
+ </md-input-container>
+
+ <div class="tb-container">
+ <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>
+ <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>
+
+
+ <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.opc-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.opc-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.opc-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.opc-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.opc-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.opc-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.opc-mapping-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.opc-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.opc-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-tooltip md-direction="top">
+ {{ 'extension.add-map' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>action.add</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.opc-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.opc-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.opc-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-tooltip md-direction="top">
+ {{ 'extension.add-timeseries' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>action.add</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-tooltip md-direction="top">
+ {{ 'extension.add-map' | translate }}
+ </md-tooltip>
+ <md-icon class="material-icons">add</md-icon>
+ <span translate>action.add</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-another-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 78fd56d..4c9f812 100644
--- a/ui/src/app/extension/index.js
+++ b/ui/src/app/extension/index.js
@@ -16,10 +16,12 @@
import ExtensionTableDirective from './extension-table.directive';
import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.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('tbExtensionFormOpc', ExtensionFormOpcDirective)
.directive('parseToNull', ParseToNull)
.name;
\ No newline at end of file
ui/src/app/locale/locale.constant.js 32(+31 -1)
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index d8b861f..6f626b5 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -773,14 +773,44 @@ 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",
+
+ "opc-field-required": "Field is required",
+ "opc-server": "Servers",
+ "opc-add-server-hint": "Add server",
+ "opc-add-server-prompt": "Please add server",
+ "opc-server-id": "Server id",
+ "opc-timeseries": "Timeseries",
+ "opc-application-name": "Application name",
+ "opc-application-uri": "Application uri",
+ "opc-host": "Host",
+ "opc-port": "Port",
+ "opc-scan-period-in-seconds": "Scan period in seconds",
+ "opc-timeout-in-millis": "Timeout in milliseconds",
+ "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-mapping":"Mapping",
+ "opc-device-node-pattern":"Device node pattern",
+ "opc-device-name-pattern":"Device name pattern",
+ "opc-mapping-attributes":"Mapping attributes",
+ "opc-username":"Username",
+ "opc-password":"Password",
+ "opc-add-another-server":"Add another server",
},
"fullscreen": {
"expand": "Expand to fullscreen",
"exit": "Exit fullscreen",
"toggle": "Toggle fullscreen mode",
- "fullscreen": "Fullscreen"
+ "fullscreen": "Fullscreen",
},
"function": {
"function": "Function"