diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 7bac29dff9..dff7635e0a 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -317,6 +317,21 @@ export default angular.module('thingsboard.types', []) name: "event.type-stats" } }, + extensionType: { + http: "HTTP", + mqtt: "MQTT", + opc: "OPC UA" + }, + extensionValueType: { + string: 'value.string', + long: 'value.long', + double: 'value.double', + boolean: 'value.boolean' + }, + extensionTransformerType: { + toDouble: 'extension.to-double', + custom: 'extension.custom' + }, latestTelemetry: { value: "LATEST_TELEMETRY", name: "attribute.scope-latest-telemetry", diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html index b8394c7958..e0760c3c94 100644 --- a/ui/src/app/device/devices.tpl.html +++ b/ui/src/app/device/devices.tpl.html @@ -67,4 +67,11 @@ entity-type="{{vm.types.entityType.device}}"> + + + + + diff --git a/ui/src/app/extension/extension-dialog.controller.js b/ui/src/app/extension/extension-dialog.controller.js new file mode 100644 index 0000000000..3b13214acd --- /dev/null +++ b/ui/src/app/extension/extension-dialog.controller.js @@ -0,0 +1,155 @@ +/* + * 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; + + 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); + } + + vm.cancel = cancel; + vm.save = save; + + function cancel() { + $mdDialog.cancel(); + } + function save() { + saveTransformers(); + if(vm.isAdd) { + vm.allExtensions.push(vm.newExtension); + } else { + var index = vm.allExtensions.indexOf(extension); + if(index > -1) { + vm.allExtensions[index] = vm.newExtension; + } + } + + 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.newExtension.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.newExtension.configuration.converterConfigurations; + if(vm.newExtension.type == types.extensionType.http) { + for(let i=0;i + +
+ +
+

{{ vm.isAdd ? 'extension.add' : 'extension.edit'}}

+ + + + +
+
+ + + +
+ +
+
+ + + +
+
extension.id-required
+
extension.unique-id-required
+
+
+ + + + + {{value}} + + +
+
extension.type-required
+
+
+
+ +
+ +
+ + +
+
+
+ + + + {{ (vm.isAdd ? 'action.add' : 'action.save') | translate }} + + {{ 'action.cancel' | translate }} + + +
+
\ 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 new file mode 100644 index 0000000000..ae28d24fbe --- /dev/null +++ b/ui/src/app/extension/extension-table.directive.js @@ -0,0 +1,240 @@ +/* + * 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); + } +} \ No newline at end of file diff --git a/ui/src/app/extension/extension-table.scss b/ui/src/app/extension/extension-table.scss new file mode 100644 index 0000000000..3d4c54a3bc --- /dev/null +++ b/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'; \ No newline at end of file diff --git a/ui/src/app/extension/extension-table.tpl.html b/ui/src/app/extension/extension-table.tpl.html new file mode 100644 index 0000000000..558b9c2a03 --- /dev/null +++ b/ui/src/app/extension/extension-table.tpl.html @@ -0,0 +1,118 @@ + + + +
+ +
+ {{ 'extension.extensions' }} + + + add + + {{ 'action.add' | translate }} + + + + search + + {{ 'action.search' | translate }} + + + + refresh + + {{ 'action.refresh' | translate }} + + +
+
+ +
+ + search + + {{ 'action.search' | translate }} + + + + + + + + close + + {{ 'action.close' | translate }} + + +
+
+ +
+ extension.selected-extensions + + + delete + + {{ 'action.delete' | translate }} + + +
+
+ + + + + + + + + + + + + + + + +
extension.idextension.type 
{{ extension.id }}{{ extension.type }} + + edit + + {{ 'extension.edit' | translate }} + + + + delete + + {{ 'extension.delete' | translate }} + + +
+
+ + +
+
+
\ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.directive.js b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js new file mode 100644 index 0000000000..a2f457290a --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-http.directive.js @@ -0,0 +1,149 @@ +/* + * 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: aceOnLoad + }; + + if(scope.isAdd) { + scope.converterConfigs = []; + scope.config.converterConfigurations = scope.converterConfigs; + } else { + scope.converterConfigs = scope.config.converterConfigurations; + } + + scope.updateValidity = function() { + var valid = scope.converterConfigs && scope.converterConfigs.length > 0; + scope.theForm.$setValidity('converterConfigs', valid); + if(scope.converterConfigs.length) { + for(let i=0;i -1) { + scope.converterConfigs.splice(index, 1); + } + } + + scope.addConverter = function(converters) { + var newConverter = {deviceNameJsonExpression:"", deviceTypeJsonExpression:"", attributes:[], timeseries:[]}; + converters.push(newConverter); + } + + scope.removeConverter = function(converter, converters) { + var index = converters.indexOf(converter); + if (index > -1) { + converters.splice(index, 1); + } + } + + scope.addAttribute = function(attributes) { + var newAttribute = {type:"", key:"", value:""}; + attributes.push(newAttribute); + } + + scope.removeAttribute = function(attribute, attributes) { + var index = attributes.indexOf(attribute); + if (index > -1) { + attributes.splice(index, 1); + } + } + + scope.transformerTypeChange = function(attribute) { + attribute.transformer = ""; + } + + function aceOnLoad(_ace) { + _ace.$blockScrolling = 1; + _ace.on("change", function(){ + var aceValue = _ace.getSession().getDocument().getValue(); + var valid = true; + if(!aceValue && !aceValue.length) { + valid = false; + } else { + try { + angular.fromJson(aceValue); + } catch(e) { + valid = false; + } + } + scope.theForm.$setValidity('transformerRequired', valid); + }); + } + + $compile(element.contents())(scope); + } + + return { + restrict: "A", + link: linker, + scope: { + config: "=", + isAdd: "=" + } + } +} \ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html new file mode 100644 index 0000000000..29d6b4f46a --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-http.tpl.html @@ -0,0 +1,309 @@ + + + + + extension.configuration + + + + + + + {{ 'extension.converter-configurations' | translate }} + + +
+ extension.add-config-prompt +
+
+
    +
  1. + + + + {{ 'action.remove' | translate }} + + + + + + + + +
    +
    extension.converter-id-required
    +
    +
    + + + + + + + + {{ 'extension.converters' | translate }} + + +
    + extension.add-converter-prompt +
    +
    +
      +
    1. + + + + {{ 'action.remove' | translate }} + + + + + + + +
      +
      extension.device-name-expression-required
      +
      +
      + + + +
      +
      extension.device-type-expression-required
      +
      +
      + + + + + {{ 'extension.attributes' | translate }} + + +
      +
        +
      1. + + + + {{ 'action.remove' | translate }} + + + + +
        + + + +
        +
        extension.required-key
        +
        +
        + + + + + {{attrTypeValue | translate}} + + +
        +
        extension.required-type
        +
        +
        +
        +
        + + + +
        +
        extension.required-value
        +
        +
        + + + + + + + {{value | translate}} + + + +
        + +
        +
        extension.transformer-json
        +
        +
        +
        +
        + +
        + + +
        +
        +
      2. +
      +
      +
      + + + {{ 'extension.add-attribute' | translate }} + + add + action.add + +
      +
      +
      +
      + + + + + + {{ 'extension.timeseries' | translate }} + + +
      +
        +
      1. + + + + {{ 'action.remove' | translate }} + + + + +
        + + + +
        +
        extension.required-key
        +
        +
        + + + + + {{attrTypeValue | translate}} + + +
        +
        extension.required-type
        +
        +
        +
        +
        + + + +
        +
        extension.required-value
        +
        +
        + + + + + + + {{value | translate}} + + + +
        + +
        +
        extension.transformer-json
        +
        +
        +
        +
        +
        + + +
        +
        +
      2. +
      +
      +
      + + + {{ 'extension.add-timeseries' | translate }} + + add + action.add + +
      +
      +
      +
      +
      +
      +
    2. +
    +
    +
    + + + {{ 'extension.add-converter' | translate }} + + add + action.add + +
    +
    +
    +
    + +
    +
    +
  2. +
+
+
+ + + {{ 'extension.add-config' | translate }} + + add + action.add + +
+
+
+
+ +
+
diff --git a/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html new file mode 100644 index 0000000000..e55ad6ca2f --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-mqtt.tpl.html @@ -0,0 +1,18 @@ + +
MQTT
\ 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 new file mode 100644 index 0000000000..779e93ab37 --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html @@ -0,0 +1,18 @@ + +
OPC UA
\ No newline at end of file diff --git a/ui/src/app/extension/extensions-forms/extension-form.scss b/ui/src/app/extension/extensions-forms/extension-form.scss new file mode 100644 index 0000000000..82d065f660 --- /dev/null +++ b/ui/src/app/extension/extensions-forms/extension-form.scss @@ -0,0 +1,41 @@ +/* + * 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 + } +} \ No newline at end of file diff --git a/ui/src/app/extension/index.js b/ui/src/app/extension/index.js new file mode 100644 index 0000000000..78fd56d824 --- /dev/null +++ b/ui/src/app/extension/index.js @@ -0,0 +1,25 @@ +/* + * 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 {ParseToNull} from './extension-dialog.controller'; + +export default angular.module('thingsboard.extension', []) + .directive('tbExtensionTable', ExtensionTableDirective) + .directive('tbExtensionFormHttp', ExtensionFormHttpDirective) + .directive('parseToNull', ParseToNull) + .name; \ No newline at end of file diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index 9d3e1d16c6..c23b0086a6 100644 --- a/ui/src/app/layout/index.js +++ b/ui/src/app/layout/index.js @@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive'; import thingsboardEntity from '../entity'; import thingsboardEvent from '../event'; import thingsboardAlarm from '../alarm'; +import thingsboardExtension from '../extension'; import thingsboardTenant from '../tenant'; import thingsboardCustomer from '../customer'; import thingsboardUser from '../user'; @@ -66,6 +67,7 @@ export default angular.module('thingsboard.home', [ thingsboardEntity, thingsboardEvent, thingsboardAlarm, + thingsboardExtension, thingsboardTenant, thingsboardCustomer, thingsboardUser, diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index bd1b9e99b0..12dbf0d323 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -729,6 +729,51 @@ export default angular.module('thingsboard.locale', []) "messages-processed": "Messages processed", "errors-occurred": "Errors occurred" }, + "extension": { + "extensions": "Extensions", + "selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected", + "type": "Type", + "key": "Key", + "value": "Value", + "id": "Id", + "extension-id": "Extension id", + "extension-type": "Extension type", + "transformer-json": "JSON*", + "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", + "attributes": "Attributes", + "add-attribute": "Add attribute", + "timeseries": "Timeseries", + "add-timeseries": "Add timeseries", + }, "fullscreen": { "expand": "Expand to fullscreen", "exit": "Exit fullscreen", @@ -1071,7 +1116,8 @@ export default angular.module('thingsboard.locale', []) "boolean": "Boolean", "boolean-value": "Boolean value", "false": "False", - "true": "True" + "true": "True", + "long": "Long" }, "widget": { "widget-library": "Widgets Library",