Browse Source

Merge branch 'feature/TB-74' of github.com:thingsboard/thingsboard into devices_attributes_syncLabel

pull/465/head
oleg 9 years ago
parent
commit
e034733a69
  1. 29
      ui/src/app/common/types.constant.js
  2. 7
      ui/src/app/device/devices.tpl.html
  3. 201
      ui/src/app/extension/extension-dialog.controller.js
  4. 84
      ui/src/app/extension/extension-dialog.tpl.html
  5. 242
      ui/src/app/extension/extension-table.directive.js
  6. 16
      ui/src/app/extension/extension-table.scss
  7. 118
      ui/src/app/extension/extension-table.tpl.html
  8. 163
      ui/src/app/extension/extensions-forms/extension-form-http.directive.js
  9. 329
      ui/src/app/extension/extensions-forms/extension-form-http.tpl.html
  10. 18
      ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html
  11. 161
      ui/src/app/extension/extensions-forms/extension-form-opc.directive.js
  12. 566
      ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
  13. 56
      ui/src/app/extension/extensions-forms/extension-form.scss
  14. 27
      ui/src/app/extension/index.js
  15. 2
      ui/src/app/layout/index.js
  16. 82
      ui/src/app/locale/locale.constant.js

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

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

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

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

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

@ -0,0 +1,201 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import beautify from 'js-beautify';
const js_beautify = beautify.js;
/*@ngInject*/
export default function ExtensionDialogController($scope, $mdDialog, $translate, isAdd, allExtensions, entityId, entityType, extension, types, attributeService) {
var vm = this;
vm.types = types;
vm.isAdd = isAdd;
vm.entityType = entityType;
vm.entityId = entityId;
vm.allExtensions = allExtensions;
if (extension) { // Editing
//vm.configuration = vm.extension.configuration;
vm.extension = angular.copy(extension);
editTransformers(vm.extension);
} else { // Add new
vm.extension = {};
}
vm.extensionTypeChange = function () {
if (vm.extension.type === "HTTP") {
vm.extension.configuration = {
"converterConfigurations": []
};
}
if (vm.extension.type === "MQTT") {
vm.extension.configuration = {
"brokers": []
};
}
if (vm.extension.type === "OPC UA") {
vm.extension.configuration = {
"servers": []
};
}
};
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
vm.save = save;
function save() {
saveTransformers();
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) - 20
}, 500);
$errorElement.eq(0).focus();
}
} else {
if(vm.isAdd) {
vm.allExtensions.push(vm.extension);
} else {
var index = vm.allExtensions.indexOf(extension);
if(index > -1) {
vm.allExtensions[index] = vm.extension;
}
}
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();
});
}
}
vm.validateId = function() {
var coincidenceArray = vm.allExtensions.filter(function(ext) {
return ext.id == vm.extension.id;
});
if(coincidenceArray.length) {
if(!vm.isAdd) {
if(coincidenceArray[0].id == extension.id) {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
}
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', false);
}
} else {
$scope.theForm.extensionId.$setValidity('uniqueIdValidation', true);
}
};
function saveTransformers() {
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++){
if(config[i].converters[j].attributes[k].transformerType == "toDouble"){
config[i].converters[j].attributes[k].transformer = {type: "intToDouble"};
}
delete config[i].converters[j].attributes[k].transformerType;
}
for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
if(config[i].converters[j].timeseries[l].transformerType == "toDouble"){
config[i].converters[j].timeseries[l].transformer = {type: "intToDouble"};
}
delete config[i].converters[j].timeseries[l].transformerType;
}
}
}
}
}
function editTransformers(extension) {
var config = extension.configuration.converterConfigurations;
if(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++){
if(config[i].converters[j].attributes[k].transformer){
if(config[i].converters[j].attributes[k].transformer.type == "intToDouble"){
config[i].converters[j].attributes[k].transformerType = "toDouble";
} else {
config[i].converters[j].attributes[k].transformerType = "custom";
config[i].converters[j].attributes[k].transformer = js_beautify(config[i].converters[j].attributes[k].transformer, {indent_size: 4});
}
}
}
for(let l=0;l<config[i].converters[j].timeseries.length;l++) {
if(config[i].converters[j].timeseries[l].transformer){
if(config[i].converters[j].timeseries[l].transformer.type == "intToDouble"){
config[i].converters[j].timeseries[l].transformerType = "toDouble";
} else {
config[i].converters[j].timeseries[l].transformerType = "custom";
config[i].converters[j].timeseries[l].transformer = js_beautify(config[i].converters[j].timeseries[l].transformer, {indent_size: 4});
}
}
}
}
}
}
}
}
/*@ngInject*/
export function ParseToNull() {
var linker = function (scope, elem, attrs, ngModel) {
ngModel.$parsers.push(function(value) {
if(value === "") {
return null;
}
return value;
})
};
return {
restrict: "A",
link: linker,
require: "ngModel"
}
}

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

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

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

@ -0,0 +1,242 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'angular-material-data-table/dist/md-data-table.min.css';
import './extension-table.scss';
/* eslint-disable import/no-unresolved, import/default */
import extensionTableTemplate from './extension-table.tpl.html';
import extensionDialogTemplate from './extension-dialog.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
import ExtensionDialogController from './extension-dialog.controller'
/*@ngInject*/
export default function ExtensionTableDirective() {
return {
restrict: "E",
scope: true,
bindToController: {
entityId: '=',
entityType: '@'
},
controller: ExtensionTableController,
controllerAs: 'vm',
templateUrl: extensionTableTemplate
};
}
/*@ngInject*/
function ExtensionTableController($scope, $filter, $document, $translate, types, $mdDialog, attributeService) {
let vm = this;
vm.extensions = [];
vm.allExtensions = [];
vm.selectedExtensions = [];
vm.extensionsCount = 0;
vm.query = {
order: 'id',
limit: 5,
page: 1,
search: null
};
vm.enterFilterMode = enterFilterMode;
vm.exitFilterMode = exitFilterMode;
vm.onReorder = onReorder;
vm.onPaginate = onPaginate;
vm.addExtension = addExtension;
vm.editExtension = editExtension;
vm.deleteExtension = deleteExtension;
vm.deleteExtensions = deleteExtensions;
vm.reloadExtensions = reloadExtensions;
vm.updateExtensions = updateExtensions;
$scope.$watch("vm.entityId", function(newVal) {
if (newVal) {
reloadExtensions();
}
});
$scope.$watch("vm.query.search", function(newVal, prevVal) {
if (!angular.equals(newVal, prevVal) && vm.query.search != null) {
updateExtensions();
}
});
function enterFilterMode() {
vm.query.search = '';
}
function exitFilterMode() {
vm.query.search = null;
updateExtensions();
}
function onReorder() {
updateExtensions();
}
function onPaginate() {
updateExtensions();
}
function addExtension($event) {
if ($event) {
$event.stopPropagation();
}
openExtensionDialog($event);
}
function editExtension($event, extension) {
if ($event) {
$event.stopPropagation();
}
openExtensionDialog($event, extension);
}
function openExtensionDialog($event, extension) {
if ($event) {
$event.stopPropagation();
}
var isAdd = false;
if(!extension) {
isAdd = true;
}
$mdDialog.show({
controller: ExtensionDialogController,
controllerAs: 'vm',
templateUrl: extensionDialogTemplate,
parent: angular.element($document[0].body),
locals: {
isAdd: isAdd,
allExtensions: vm.allExtensions,
entityId: vm.entityId,
entityType: vm.entityType,
extension: extension
},
bindToController: true,
targetEvent: $event,
fullscreen: true,
skipHide: true
}).then(function() {
reloadExtensions();
}, function () {
});
}
function deleteExtension($event, extension) {
if ($event) {
$event.stopPropagation();
}
if(extension) {
var title = $translate.instant('extension.delete-extension-title', {extensionId: extension.id});
var content = $translate.instant('extension.delete-extension-text');
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title(title)
.htmlContent(content)
.ariaLabel(title)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function() {
var editedExtensions = vm.allExtensions.filter(function(ext) {
return ext.id !== extension.id;
});
var editedValue = angular.toJson(editedExtensions);
attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
function success() {
reloadExtensions();
}
);
});
}
}
function deleteExtensions($event) {
if ($event) {
$event.stopPropagation();
}
if (vm.selectedExtensions && vm.selectedExtensions.length > 0) {
var title = $translate.instant('extension.delete-extensions-title', {count: vm.selectedExtensions.length}, 'messageformat');
var content = $translate.instant('extension.delete-extensions-text');
var confirm = $mdDialog.confirm()
.targetEvent($event)
.title(title)
.htmlContent(content)
.ariaLabel(title)
.cancel($translate.instant('action.no'))
.ok($translate.instant('action.yes'));
$mdDialog.show(confirm).then(function () {
var editedExtensions = angular.copy(vm.allExtensions);
for (var i = 0; i < vm.selectedExtensions.length; i++) {
editedExtensions = editedExtensions.filter(function (ext) {
return ext.id !== vm.selectedExtensions[i].id;
});
}
var editedValue = angular.toJson(editedExtensions);
attributeService.saveEntityAttributes(vm.entityType, vm.entityId, types.attributesScope.shared.value, [{key:"configuration", value:editedValue}]).then(
function success() {
reloadExtensions();
}
);
});
}
}
function reloadExtensions() {
vm.allExtensions.length = 0;
vm.extensions.length = 0;
vm.extensionsPromise = attributeService.getEntityAttributesValues(vm.entityType, vm.entityId, types.attributesScope.shared.value, ["configuration"]);
vm.extensionsPromise.then(
function success(data) {
vm.allExtensions = angular.fromJson(data[0].value);
vm.selectedExtensions = [];
updateExtensions();
vm.extensionsPromise = null;
},
function fail() {
vm.extensions = [];
vm.selectedExtensions = [];
updateExtensions();
vm.extensionsPromise = null;
}
);
}
function updateExtensions() {
vm.selectedExtensions = [];
var result = $filter('orderBy')(vm.allExtensions, vm.query.order);
if (vm.query.search != null) {
result = $filter('filter')(result, function(extension) {
if(!vm.query.search || (extension.id.indexOf(vm.query.search) != -1) || (extension.type.indexOf(vm.query.search) != -1)) {
return true;
}
return false;
});
}
vm.extensionsCount = result.length;
var startIndex = vm.query.limit * (vm.query.page - 1);
vm.extensions = result.slice(startIndex, startIndex + vm.query.limit);
}
}

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

@ -0,0 +1,16 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../scss/constants';

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

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

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

@ -0,0 +1,163 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/theme/github';
import './extension-form.scss';
/* eslint-disable angular/log */
import extensionFormHttpTemplate from './extension-form-http.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ExtensionFormHttpDirective($compile, $templateCache, $translate, types) {
var linker = function(scope, element) {
var template = $templateCache.get(extensionFormHttpTemplate);
element.html(template);
scope.types = types;
scope.theForm = scope.$parent.theForm;
scope.extensionCustomTransformerOptions = {
useWrapMode: false,
mode: 'json',
showGutter: true,
showPrintMargin: true,
theme: 'github',
advanced: {
enableSnippets: true,
enableBasicAutocompletion: true,
enableLiveAutocompletion: true
},
onLoad: function(_ace) {
_ace.$blockScrolling = 1;
}
};
scope.addConverterConfig = function() {
var newConverterConfig = {converterId:"", converters:[]};
scope.converterConfigs.push(newConverterConfig);
scope.converterConfigs[scope.converterConfigs.length - 1].converters = [];
scope.addConverter(scope.converterConfigs[scope.converterConfigs.length - 1].converters);
};
scope.removeConverterConfig = function(config) {
var index = scope.converterConfigs.indexOf(config);
if (index > -1) {
scope.converterConfigs.splice(index, 1);
}
scope.theForm.$setDirty();
};
scope.addConverter = function(converters) {
var newConverter = {
deviceNameJsonExpression:"",
deviceTypeJsonExpression:"",
attributes:[],
timeseries:[]
};
converters.push(newConverter);
};
scope.removeConverter = function(converter, converters) {
var index = converters.indexOf(converter);
if (index > -1) {
converters.splice(index, 1);
}
scope.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.updateValidity = function() {
let 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.transformerTypeChange = function(attribute) {
attribute.transformer = "";
};
scope.validateTransformer = function (model, editorName) {
if(model && model.length) {
try {
angular.fromJson(model);
scope.theForm[editorName].$setValidity('transformerJSON', true);
} catch(e) {
scope.theForm[editorName].$setValidity('transformerJSON', false);
}
}
};
$compile(element.contents())(scope);
};
return {
restrict: "A",
link: linker,
scope: {
config: "=",
isAdd: "="
}
}
}

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

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

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

@ -0,0 +1,18 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div>MQTT</div>

161
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: "="
}
}
}

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

@ -0,0 +1,566 @@
<!--
Copyright © 2016-2017 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-card class="extension-form extension-opc">
<md-card-title>
<md-card-title-text>
<span translate class="md-headline">extension.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<v-accordion id="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" 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>
<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>

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

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.extension-form {
li > .md-button {
color: rgba(0, 0, 0, 0.7);
margin: 0;
}
.vAccordion--default {
margin-top: 0;
padding-left: 3px;
}
}
.tb-extension-custom-transformer-panel {
margin-left: 15px;
border: 1px solid #C0C0C0;
height: 100%;
.tb-extension-custom-transformer {
min-width: 600px;
min-height: 200px;
width: 100%;
height: 100%;
}
.ace_text-input {
position:absolute!important
}
}
.extensionDialog {
min-width: 1000px;
}
.tb-container-for-select {
height: 58px;
}
.tb-drop-file-input-hide {
height: 200%;
display: block;
position: absolute;
bottom: 0;
width: 100%;
}

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

@ -0,0 +1,27 @@
/*
* Copyright © 2016-2017 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;

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

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

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

@ -729,11 +729,88 @@ export default angular.module('thingsboard.locale', [])
"messages-processed": "Messages processed", "messages-processed": "Messages processed",
"errors-occurred": "Errors occurred" "errors-occurred": "Errors occurred"
}, },
"extension": {
"extensions": "Extensions",
"selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected",
"type": "Type",
"key": "Key",
"value": "Value",
"id": "Id",
"extension-id": "Extension id",
"extension-type": "Extension type",
"transformer-json": "JSON*",
"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",
"delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
"delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
"delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?",
"delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
"converters": "Converters",
"converter-id": "Converter id",
"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",
"json-required": "Transformer json is required.",
"json-parse": "Unable to parse transformer json.",
"attributes": "Attributes",
"add-attribute": "Add attribute",
"add-map": "Add mapping element",
"timeseries": "Timeseries",
"add-timeseries": "Add timeseries",
"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": { "fullscreen": {
"expand": "Expand to fullscreen", "expand": "Expand to fullscreen",
"exit": "Exit fullscreen", "exit": "Exit fullscreen",
"toggle": "Toggle fullscreen mode", "toggle": "Toggle fullscreen mode",
"fullscreen": "Fullscreen" "fullscreen": "Fullscreen",
}, },
"function": { "function": {
"function": "Function" "function": "Function"
@ -1071,7 +1148,8 @@ export default angular.module('thingsboard.locale', [])
"boolean": "Boolean", "boolean": "Boolean",
"boolean-value": "Boolean value", "boolean-value": "Boolean value",
"false": "False", "false": "False",
"true": "True" "true": "True",
"long": "Long"
}, },
"widget": { "widget": {
"widget-library": "Widgets Library", "widget-library": "Widgets Library",

Loading…
Cancel
Save