diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 7267f0e223..125674ecfe 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -828,6 +828,10 @@ export default angular.module('thingsboard.types', []) custom: { name: 'widget-action.custom', value: 'custom' + }, + customPretty: { + name: 'widget-action.custom-pretty', + value: 'customPretty' } }, systemBundleAlias: { diff --git a/ui/src/app/components/widget/action/custom-action-pretty-editor.directive.js b/ui/src/app/components/widget/action/custom-action-pretty-editor.directive.js new file mode 100644 index 0000000000..c6a8c725dd --- /dev/null +++ b/ui/src/app/components/widget/action/custom-action-pretty-editor.directive.js @@ -0,0 +1,993 @@ +/* + * Copyright © 2016-2019 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 './custom-action-pretty-editor.scss'; +import customActionPrettyEditorTemplate from './custom-action-pretty-editor.tpl.html'; + +import 'brace/ext/language_tools'; +import 'brace/ext/searchbox'; +import 'brace/mode/html'; +import 'brace/mode/css'; +import 'brace/snippets/text'; +import 'brace/snippets/html'; +import 'brace/snippets/css'; + +import beautify from 'js-beautify'; +import Split from "split.js"; + +const html_beautify = beautify.html; +const css_beautify = beautify.css; + +export default angular.module('thingsboard.directives.customActionPrettyEditor', []) + .directive('tbCustomActionPrettyEditor', CustomActionPrettyEditor) + .name; + +/*@ngInject*/ +function CustomActionPrettyEditor($compile, $templateCache, $window, $timeout) { + + var linker = function (scope, element, attrs, ngModelCtrl) { + var template = $templateCache.get(customActionPrettyEditorTemplate); + element.html(template); + var ace_editors = []; + scope.fullscreen = false; + scope.htmlEditorOptions = { + useWrapMode: true, + mode: 'html', + advanced: { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }, + onLoad: function (_ace) { + ace_editors.push(_ace); + } + }; + scope.cssEditorOptions = { + useWrapMode: true, + mode: 'css', + advanced: { + enableSnippets: true, + enableBasicAutocompletion: true, + enableLiveAutocompletion: true + }, + onLoad: function (_ace) { + ace_editors.push(_ace); + } + }; + + scope.addResource = addResource; + scope.beautifyCss = beautifyCss; + scope.beautifyHtml = beautifyHtml; + scope.removeResource = removeResource; + scope.toggleFullscreen = toggleFullscreen; + + var sampleJsFunction = "/* There are three examples: for delete, edit and add entity */\n" + + "/* Delete entity example */\n" + + "//\n" + + "//var $injector = widgetContext.$scope.$injector;\n" + + "//var $mdDialog = $injector.get('$mdDialog'),\n" + + "// $document = $injector.get('$document'),\n" + + "// types = $injector.get('types'),\n" + + "// assetService = $injector.get('assetService'),\n" + + "// deviceService = $injector.get('deviceService')\n" + + "// $rootScope = $injector.get('$rootScope'),\n" + + "// $q = $injector.get('$q');\n" + + "//\n" + + "//openDeleteEntityDialog();\n" + + "//\n" + + "//function openDeleteEntityDialog() {\n" + + "// var title = 'Delete ' + entityId.entityType\n" + + "// .toLowerCase() + ' ' +\n" + + "// entityName;\n" + + "// var content = 'Are you sure you want to delete the ' +\n" + + "// entityId.entityType.toLowerCase() + ' ' +\n" + + "// entityName + '?';\n" + + "// var confirm = $mdDialog.confirm()\n" + + "// .targetEvent($event)\n" + + "// .title(title)\n" + + "// .htmlContent(content)\n" + + "// .ariaLabel(title)\n" + + "// .cancel('Cancel')\n" + + "// .ok('Delete');\n" + + "// $mdDialog.show(confirm).then(function() {\n" + + "// deleteEntity();\n" + + "// })\n" + + "//}\n" + + "//\n" + + "//function deleteEntity() {\n" + + "// deleteEntityPromise(entityId).then(\n" + + "// function success() {\n" + + "// updateAliasData();\n" + + "// },\n" + + "// function fail() {\n" + + "// showErrorDialog();\n" + + "// }\n" + + "// );\n" + + "//}\n" + + "//\n" + + "//function deleteEntityPromise(entityId) {\n" + + "// if (entityId.entityType == types.entityType.asset) {\n" + + "// return assetService.deleteAsset(entityId.id);\n" + + "// } else if (entityId.entityType == types.entityType.device) {\n" + + "// return deviceService.deleteDevice(entityId.id);\n" + + "// }\n" + + "//}\n" + + "//\n" + + "//function updateAliasData() {\n" + + "// var aliasIds = [];\n" + + "// for (var id in widgetContext.aliasController.resolvedAliases) {\n" + + "// aliasIds.push(id);\n" + + "// }\n" + + "// var tasks = [];\n" + + "// aliasIds.forEach(function(aliasId) {\n" + + "// widgetContext.aliasController.setAliasUnresolved(aliasId);\n" + + "// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n" + + "// });\n" + + "// $q.all(tasks).then(function() {\n" + + "// $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n" + + "// });\n" + + "//}\n" + + "//\n" + + "//function showErrorDialog() {\n" + + "// var title = 'Error';\n" + + "// var content = 'An error occurred while deleting the entity. Please try again.';\n" + + "// var alert = $mdDialog.alert()\n" + + "// .title(title)\n" + + "// .htmlContent(content)\n" + + "// .ariaLabel(title)\n" + + "// .parent(angular.element($document[0].body))\n" + + "// .targetEvent($event)\n" + + "// .multiple(true)\n" + + "// .clickOutsideToClose(true)\n" + + "// .ok('CLOSE');\n" + + "// $mdDialog.show(alert);\n" + + "//}\n" + + "//\n" + + "/* Edit entity example */\n" + + "//\n" + + "//var $injector = widgetContext.$scope.$injector;\n" + + "//var $mdDialog = $injector.get('$mdDialog'),\n" + + "// $document = $injector.get('$document'),\n" + + "// $q = $injector.get('$q'),\n" + + "// types = $injector.get('types'),\n" + + "// $rootScope = $injector.get('$rootScope'),\n" + + "// entityService = $injector.get('entityService'),\n" + + "// attributeService = $injector.get('attributeService'),\n" + + "// entityRelationService = $injector.get('entityRelationService');\n" + + "//\n" + + "//openEditEntityDialog();\n" + + "//\n" + + "//function openEditEntityDialog() {\n" + + "// $mdDialog.show({\n" + + "// controller: ['$scope','$mdDialog', EditEntityDialogController],\n" + + "// controllerAs: 'vm',\n" + + "// template: htmlTemplate,\n" + + "// locals: {\n" + + "// entityId: entityId\n" + + "// },\n" + + "// parent: angular.element($document[0].body),\n" + + "// targetEvent: $event,\n" + + "// multiple: true,\n" + + "// clickOutsideToClose: false\n" + + "// });\n" + + "//}\n" + + "//\n" + + "//function EditEntityDialogController($scope,$mdDialog) {\n" + + "// var vm = this;\n" + + "// vm.entityId = entityId;\n" + + "// vm.entityName = entityName;\n" + + "// vm.entityType = entityId.entityType;\n" + + "// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device];\n" + + "// vm.allowedRelatedEntityTypes = [];\n" + + "// vm.entitySearchDirection = types.entitySearchDirection;\n" + + "// vm.attributes = {};\n" + + "// vm.serverAttributes = {};\n" + + "// vm.relations = [];\n" + + "// vm.newRelations = [];\n" + + "// vm.relationsToDelete = [];\n" + + "// getEntityInfo();\n" + + "// \n" + + "// vm.addRelation = function() {\n" + + "// var relation = {\n" + + "// direction: null,\n" + + "// relationType: null,\n" + + "// relatedEntity: null\n" + + "// };\n" + + "// vm.newRelations.push(relation);\n" + + "// $scope.editEntityForm.$setDirty();\n" + + "// };\n" + + "// vm.removeRelation = function(index) {\n" + + "// if (index > -1) {\n" + + "// vm.newRelations.splice(index, 1);\n" + + "// $scope.editEntityForm.$setDirty();\n" + + "// }\n" + + "// };\n" + + "// vm.removeOldRelation = function(index, relation) {\n" + + "// if (index > -1) {\n" + + "// vm.relations.splice(index, 1);\n" + + "// vm.relationsToDelete.push(relation);\n" + + "// $scope.editEntityForm.$setDirty();\n" + + "// }\n" + + "// };\n" + + "// vm.save = function() {\n" + + "// saveAttributes();\n" + + "// saveRelations();\n" + + "// $scope.editEntityForm.$setPristine();\n" + + "// };\n" + + "// vm.cancel = function() {\n" + + "// $mdDialog.hide();\n" + + "// };\n" + + "// \n" + + "// function getEntityAttributes(attributes) {\n" + + "// for (var i = 0; i < attributes.length; i++) {\n" + + "// vm.attributes[attributes[i].key] = attributes[i].value; \n" + + "// }\n" + + "// vm.serverAttributes = angular.copy(vm.attributes);\n" + + "// }\n" + + "// \n" + + "// function getEntityRelations(relations) {\n" + + "// var relationsFrom = relations[0];\n" + + "// var relationsTo = relations[1];\n" + + "// for (var i=0; i < relationsFrom.length; i++) {\n" + + "// var relation = {\n" + + "// direction: types.entitySearchDirection.from,\n" + + "// relationType: relationsFrom[i].type,\n" + + "// relatedEntity: relationsFrom[i].to\n" + + "// };\n" + + "// vm.relations.push(relation);\n" + + "// }\n" + + "// for (var i=0; i < relationsTo.length; i++) {\n" + + "// var relation = {\n" + + "// direction: types.entitySearchDirection.to,\n" + + "// relationType: relationsTo[i].type,\n" + + "// relatedEntity: relationsTo[i].from\n" + + "// };\n" + + "// vm.relations.push(relation);\n" + + "// }\n" + + "// }\n" + + "// \n" + + "// function getEntityInfo() {\n" + + "// entityService.getEntity(entityId.entityType, entityId.id).then(\n" + + "// function(entity) {\n" + + "// vm.entity = entity;\n" + + "// vm.type = vm.entity.type;\n" + + "// });\n" + + "// attributeService.getEntityAttributesValues(entityId.entityType, entityId.id, 'SERVER_SCOPE').then(\n" + + "// function(data){\n" + + "// if (data.length) {\n" + + "// getEntityAttributes(data);\n" + + "// }\n" + + "// });\n" + + "// $q.all([entityRelationService.findInfoByFrom(entityId.id, entityId.entityType), entityRelationService.findInfoByTo(entityId.id, entityId.entityType)]).then(\n" + + "// function(relations){\n" + + "// getEntityRelations(relations);\n" + + "// });\n" + + "// }\n" + + "// \n" + + "// function saveAttributes() {\n" + + "// var attributesArray = [];\n" + + "// for (var key in vm.attributes) {\n" + + "// if (vm.attributes[key] !== vm.serverAttributes[key]) {\n" + + "// attributesArray.push({key: key, value: vm.attributes[key]});\n" + + "// }\n" + + "// }\n" + + "// if (attributesArray.length > 0) {\n" + + "// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);\n" + + "// } \n" + + "// }\n" + + "// \n" + + "// function saveRelations() {\n" + + "// var tasks = [];\n" + + "// for (var i=0; i < vm.newRelations.length; i++) {\n" + + "// var relation = {\n" + + "// type: vm.newRelations[i].relationType\n" + + "// };\n" + + "// if (vm.newRelations[i].direction == types.entitySearchDirection.from) {\n" + + "// relation.to = vm.newRelations[i].relatedEntity;\n" + + "// relation.from = entityId;\n" + + "// } else {\n" + + "// relation.to = entityId;\n" + + "// relation.from = vm.newRelations[i].relatedEntity;\n" + + "// }\n" + + "// tasks.push(entityRelationService.saveRelation(relation));\n" + + "// }\n" + + "// for (var i=0; i < vm.relationsToDelete.length; i++) {\n" + + "// var relation = {\n" + + "// type: vm.relationsToDelete[i].relationType\n" + + "// };\n" + + "// if (vm.relationsToDelete[i].direction == types.entitySearchDirection.from) {\n" + + "// relation.to = vm.relationsToDelete[i].relatedEntity;\n" + + "// relation.from = entityId;\n" + + "// } else {\n" + + "// relation.to = entityId;\n" + + "// relation.from = vm.relationsToDelete[i].relatedEntity;\n" + + "// }\n" + + "// tasks.push(entityRelationService.deleteRelation(relation.from.id, relation.from.entityType, relation.type, relation.to.id, relation.to.entityType));\n" + + "// }\n" + + "// $q.all(tasks).then(function(){\n" + + "// vm.relations = vm.relations.concat(vm.newRelations);\n" + + "// vm.newRelations = [];\n" + + "// vm.relationsToDelete = [];\n" + + "// updateAliasData();\n" + + "// });\n" + + "// }\n" + + "// \n" + + "// function updateAliasData() {\n" + + "// var aliasIds = [];\n" + + "// for (var id in widgetContext.aliasController.resolvedAliases) {\n" + + "// aliasIds.push(id);\n" + + "// }\n" + + "// var tasks = [];\n" + + "// aliasIds.forEach(function(aliasId) {\n" + + "// widgetContext.aliasController.setAliasUnresolved(aliasId);\n" + + "// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n" + + "// });\n" + + "// $q.all(tasks).then(function() {\n" + + "// $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n" + + "// });\n" + + "// }\n" + + "//}\n" + + "//\n" + + "/* Add entity example */\n" + + "//\n" + + "//var $injector = widgetContext.$scope.$injector;\n" + + "//var $mdDialog = $injector.get('$mdDialog'),\n" + + "// $document = $injector.get('$document'),\n" + + "// $q = $injector.get('$q'),\n" + + "// $rootScope = $injector.get('$rootScope'),\n" + + "// types = $injector.get('types'),\n" + + "// assetService = $injector.get('assetService'),\n" + + "// deviceService = $injector.get('deviceService'),\n" + + "// attributeService = $injector.get('attributeService'),\n" + + "// entityRelationService = $injector.get('entityRelationService');\n" + + "//\n" + + "//openAddEntityDialog();\n" + + "//\n" + + "//function openAddEntityDialog() {\n" + + "// $mdDialog.show({\n" + + "// controller: ['$scope','$mdDialog', AddEntityDialogController],\n" + + "// controllerAs: 'vm',\n" + + "// template: htmlTemplate,\n" + + "// locals: {\n" + + "// entityId: entityId\n" + + "// },\n" + + "// parent: angular.element($document[0].body),\n" + + "// targetEvent: $event,\n" + + "// multiple: true,\n" + + "// clickOutsideToClose: false\n" + + "// });\n" + + "//}\n" + + "//\n" + + "//function AddEntityDialogController($scope, $mdDialog) {\n" + + "// var vm = this;\n" + + "// vm.allowedEntityTypes = [types.entityType.asset, types.entityType.device];\n" + + "// vm.allowedRelatedEntityTypes = [];\n" + + "// vm.entitySearchDirection = types.entitySearchDirection;\n" + + "// vm.attributes = {};\n" + + "// vm.relations = [];\n" + + "// \n" + + "// vm.addRelation = function() {\n" + + "// var relation = {\n" + + "// direction: null,\n" + + "// relationType: null,\n" + + "// relatedEntity: null\n" + + "// };\n" + + "// vm.relations.push(relation);\n" + + "// };\n" + + "// vm.removeRelation = function(index) {\n" + + "// if (index > -1) {\n" + + "// vm.relations.splice(index, 1);\n" + + "// }\n" + + "// };\n" + + "// vm.save = function() {\n" + + "// $scope.addEntityForm.$setPristine();\n" + + "// saveEntityPromise().then(\n" + + "// function (entity) {\n" + + "// saveAttributes(entity.id);\n" + + "// saveRelations(entity.id);\n" + + "// $mdDialog.hide();\n" + + "// }\n" + + "// );\n" + + "// };\n" + + "// vm.cancel = function() {\n" + + "// $mdDialog.hide();\n" + + "// };\n" + + "// \n" + + "// \n" + + "// function saveEntityPromise() {\n" + + "// var entity = {\n" + + "// name: vm.entityName,\n" + + "// type: vm.type\n" + + "// };\n" + + "// if (vm.entityType == types.entityType.asset) {\n" + + "// return assetService.saveAsset(entity);\n" + + "// } else if (vm.entityType == types.entityType.device) {\n" + + "// return deviceService.saveDevice(entity);\n" + + "// }\n" + + "// }\n" + + "// \n" + + "// function saveAttributes(entityId) {\n" + + "// var attributesArray = [];\n" + + "// for (var key in vm.attributes) {\n" + + "// attributesArray.push({key: key, value: vm.attributes[key]});\n" + + "// }\n" + + "// if (attributesArray.length > 0) {\n" + + "// attributeService.saveEntityAttributes(entityId.entityType, entityId.id, \"SERVER_SCOPE\", attributesArray);\n" + + "// } \n" + + "// }\n" + + "// \n" + + "// function saveRelations(entityId) {\n" + + "// var tasks = [];\n" + + "// for (var i=0; i < vm.relations.length; i++) {\n" + + "// var relation = {\n" + + "// type: vm.relations[i].relationType\n" + + "// };\n" + + "// if (vm.relations[i].direction == types.entitySearchDirection.from) {\n" + + "// relation.to = vm.relations[i].relatedEntity;\n" + + "// relation.from = entityId;\n" + + "// } else {\n" + + "// relation.to = entityId;\n" + + "// relation.from = vm.relations[i].relatedEntity;\n" + + "// }\n" + + "// tasks.push(entityRelationService.saveRelation(relation));\n" + + "// }\n" + + "// $q.all(tasks).then(function(){\n" + + "// updateAliasData();\n" + + "// });\n" + + "// }\n" + + "// \n" + + "// function updateAliasData() {\n" + + "// var aliasIds = [];\n" + + "// for (var id in widgetContext.aliasController.resolvedAliases) {\n" + + "// aliasIds.push(id);\n" + + "// }\n" + + "// var tasks = [];\n" + + "// aliasIds.forEach(function(aliasId) {\n" + + "// widgetContext.aliasController.setAliasUnresolved(aliasId);\n" + + "// tasks.push(widgetContext.aliasController.getAliasInfo(aliasId));\n" + + "// });\n" + + "// $q.all(tasks).then(function() {\n" + + "// $rootScope.$broadcast('entityAliasesChanged', aliasIds);\n" + + "// });\n" + + "// }\n" + + "//}\n" + + "\n" + + "\n"; + + var sampleHtmlTemplate = '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n' + + '\n'; + + var sampleCss = '/* There are two examples: for edit and add entity */\n' + + '/* Edit entity example */\n' + + '/*\n' + + '.edit-entity-form md-input-container {\n' + + ' padding-right: 10px;\n' + + '}\n' + + '\n' + + '.edit-entity-form .boolean-value-input {\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.edit-entity-form .boolean-value-input .checkbox-label {\n' + + ' margin-bottom: 8px;\n' + + ' color: rgba(0,0,0,0.54);\n' + + ' font-size: 12px;\n' + + '}\n' + + '\n' + + '.relations-list .header {\n' + + ' padding-right: 5px;\n' + + ' padding-bottom: 5px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .header .cell {\n' + + ' padding-right: 5px;\n' + + ' padding-left: 5px;\n' + + ' font-size: 12px;\n' + + ' font-weight: 700;\n' + + ' color: rgba(0, 0, 0, .54);\n' + + ' white-space: nowrap;\n' + + '}\n' + + '\n' + + '.relations-list .body {\n' + + ' padding-right: 5px;\n' + + ' padding-bottom: 15px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body .row {\n' + + ' padding-top: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body .cell {\n' + + ' padding-right: 5px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body md-autocomplete-wrap md-input-container {\n' + + ' height: 30px;\n' + + '}\n' + + '\n' + + '.relations-list .body .md-button {\n' + + ' margin: 0;\n' + + '}\n' + + '\n' + + '.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {\n' + + ' display: none;\n' + + '} \n' + + '*/\n' + + '/* Add entity example */\n' + + '/*\n' + + '.add-entity-form md-input-container {\n' + + ' padding-right: 10px;\n' + + '}\n' + + '\n' + + '.add-entity-form .boolean-value-input {\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.add-entity-form .boolean-value-input .checkbox-label {\n' + + ' margin-bottom: 8px;\n' + + ' color: rgba(0,0,0,0.54);\n' + + ' font-size: 12px;\n' + + '}\n' + + '\n' + + '.relations-list .header {\n' + + ' padding-right: 5px;\n' + + ' padding-bottom: 5px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .header .cell {\n' + + ' padding-right: 5px;\n' + + ' padding-left: 5px;\n' + + ' font-size: 12px;\n' + + ' font-weight: 700;\n' + + ' color: rgba(0, 0, 0, .54);\n' + + ' white-space: nowrap;\n' + + '}\n' + + '\n' + + '.relations-list .body {\n' + + ' padding-right: 5px;\n' + + ' padding-bottom: 15px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body .row {\n' + + ' padding-top: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body .cell {\n' + + ' padding-right: 5px;\n' + + ' padding-left: 5px;\n' + + '}\n' + + '\n' + + '.relations-list .body md-autocomplete-wrap md-input-container {\n' + + ' height: 30px;\n' + + '}\n' + + '\n' + + '.relations-list .body .md-button {\n' + + ' margin: 0;\n' + + '}\n' + + '*/\n' + + '\n' + + '\n'; + + scope.$watch('action', function () { + ngModelCtrl.$setViewValue(scope.action); + }); + + ngModelCtrl.$render = function () { + scope.action = ngModelCtrl.$viewValue; + if (angular.isUndefined(scope.action.customHtml) && angular.isUndefined(scope.action.customCss) && angular.isUndefined(scope.action.customFunction)) { + scope.action.customFunction = sampleJsFunction; + scope.action.customHtml = sampleHtmlTemplate; + scope.action.customCss = sampleCss; + } + }; + + function removeResource(index) { + if (index > -1) { + scope.action.customResources.splice(index, 1); + scope.theForm.$setDirty(); + } + } + + function addResource() { + if (!scope.action.customResources) { + scope.action.customResources = []; + } + scope.action.customResources.push({url: ''}); + scope.theForm.$setDirty(); + } + + function beautifyHtml() { + var res = html_beautify(scope.action.customHtml, {indent_size: 4, wrap_line_length: 60}); + scope.action.customHtml = res; + } + + function beautifyCss() { + var res = css_beautify(scope.action.customCss, {indent_size: 4}); + scope.action.customCss = res; + } + + function toggleFullscreen() { + scope.fullscreen = !scope.fullscreen; + if (scope.fullscreen) { + scope.customActionEditorElement = angular.element('.tb-custom-action-editor'); + angular.element(scope.customActionEditorElement[0]).ready(function () { + var w = scope.customActionEditorElement.width(); + if (w > 0) { + initSplitLayout(); + } else { + scope.$watch( + function () { + return scope.customActionEditorElement[0].offsetWidth || parseInt(scope.customActionEditorElement.css('width'), 10); + }, + function (newSize) { + if (newSize > 0) { + initSplitLayout(); + } + } + ); + } + }); + } else { + scope.layoutInited = false; + } + } + + function onDividerDrag() { + scope.$broadcast('update-ace-editor-size'); + for (var i = 0; i < ace_editors.length; i++) { + var ace = ace_editors[i]; + ace.resize(); + ace.renderer.updateFull(); + } + } + + function initSplitLayout() { + if (!scope.layoutInited) { + Split([angular.element('#left-panel', scope.customActionEditorElement)[0], angular.element('#right-panel', scope.customActionEditorElement)[0]], { + sizes: [50, 50], + gutterSize: 8, + cursor: 'col-resize', + onDrag: function () { + onDividerDrag() + } + }); + + onDividerDrag(); + + scope.$applyAsync(function () { + scope.layoutInited = true; + var w = angular.element($window); + $timeout(function () { + w.triggerHandler('resize') + }); + }); + + } + } + + $compile(element.contents())(scope); + }; + + return { + restrict: "E", + require: "^ngModel", + scope: { + theForm: '=?', + }, + link: linker + }; +} diff --git a/ui/src/app/components/widget/action/custom-action-pretty-editor.scss b/ui/src/app/components/widget/action/custom-action-pretty-editor.scss new file mode 100644 index 0000000000..6c977a1b1a --- /dev/null +++ b/ui/src/app/components/widget/action/custom-action-pretty-editor.scss @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2019 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.tb-custom-action-pretty { + box-sizing: border-box; + padding: 8px; + background-color: #fff; + + .tb-fullscreen-panel { + .tb-custom-action-editor-container { + height: calc(100% - 40px); + } + + #right-panel { + padding-top: 8px; + padding-left: 3px; + } + + tb-js-func .tb-js-func-panel { + box-sizing: border-box; + } + + md-tabs-content-wrapper, + md-tab-content { + height: 100%; + } + } + + .html-panel, + .css-panel { + width: 100%; + min-width: 200px; + height: 100%; + min-height: 200px; + } + + .tb-split { + box-sizing: border-box; + overflow-x: hidden; + overflow-y: auto; + } + + .tb-content { + border: 1px solid #c0c0c0; + } + + .gutter { + background-color: #eee; + background-repeat: no-repeat; + background-position: 50%; + } + + .gutter.gutter-horizontal { + cursor: col-resize; + background-image: url("../../../../../node_modules/split.js/grips/vertical.png"); + } + + .tb-split.tb-split-horizontal, + .gutter.gutter-horizontal { + float: left; + height: 100%; + } + + .tb-action-expand-button { + position: absolute; + right: 14px; + z-index: 1; + + &.tb-fullscreen-editor { + position: relative; + right: 0; + } + + .md-button { + min-width: auto; + } + } + + .tb-custom-action-editor { + &.tb-fullscreen-editor { + height: 100%; + } + } +} diff --git a/ui/src/app/components/widget/action/custom-action-pretty-editor.tpl.html b/ui/src/app/components/widget/action/custom-action-pretty-editor.tpl.html new file mode 100644 index 0000000000..80cdf2a786 --- /dev/null +++ b/ui/src/app/components/widget/action/custom-action-pretty-editor.tpl.html @@ -0,0 +1,186 @@ + +
+
+ + + {{ 'widget.toggle-fullscreen' | translate }} + + + fullscreen + + + fullscreen_exit + + widget.toggle-fullscreen + +
+
+
+ + +
+
+
+ + + + + + {{ 'widget.remove-resource' | translate }} + + + close + + +
+
+ + + {{ 'widget.add-resource' | translate }} + + action.add + +
+
+
+
+ +
+
+ {{ 'widget.tidy' | translate }} + + +
+
+
+
+ +
+
+ {{ 'widget.tidy' | translate }} + + +
+
+
+
+ + + + +
+
+
+
+ + +
+
+
+ + + + + + {{ 'widget.remove-resource' | translate }} + + + close + + +
+
+ + + {{ 'widget.add-resource' | translate }} + + action.add + +
+
+
+
+ +
+
+ {{ 'widget.tidy' | translate }} + + +
+
+
+
+ +
+
+ {{ 'widget.tidy' | translate }} + + +
+
+
+
+
+
+
+ + +
+
+
+
\ No newline at end of file diff --git a/ui/src/app/components/widget/action/manage-widget-actions.directive.js b/ui/src/app/components/widget/action/manage-widget-actions.directive.js index bcaa96aea2..25c7df74a8 100644 --- a/ui/src/app/components/widget/action/manage-widget-actions.directive.js +++ b/ui/src/app/components/widget/action/manage-widget-actions.directive.js @@ -37,6 +37,7 @@ import './manage-widget-actions.scss'; import thingsboardMaterialIconSelect from '../../material-icon-select.directive'; import WidgetActionDialogController from './widget-action-dialog.controller'; +import CustomActionPrettyEditor from './custom-action-pretty-editor.directive'; /* eslint-disable import/no-unresolved, import/default */ @@ -45,7 +46,7 @@ import widgetActionDialogTemplate from './widget-action-dialog.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ -export default angular.module('thingsboard.directives.widgetActions', [thingsboardMaterialIconSelect]) +export default angular.module('thingsboard.directives.widgetActions', [thingsboardMaterialIconSelect, CustomActionPrettyEditor]) .controller('WidgetActionDialogController', WidgetActionDialogController) .directive('tbManageWidgetActions', ManageWidgetActions) .name; diff --git a/ui/src/app/components/widget/action/widget-action-dialog.controller.js b/ui/src/app/components/widget/action/widget-action-dialog.controller.js index d2ac7fbb1f..22afa80210 100644 --- a/ui/src/app/components/widget/action/widget-action-dialog.controller.js +++ b/ui/src/app/components/widget/action/widget-action-dialog.controller.js @@ -14,7 +14,7 @@ * limitations under the License. */ /*@ngInject*/ -export default function WidgetActionDialogController($scope, $mdDialog, $filter, $q, dashboardService, dashboardUtils, types, utils, +export default function WidgetActionDialogController($scope, $mdDialog, $filter, $q, dashboardService, dashboardUtils, types, toast, utils, isAdd, fetchDashboardStates, actionSources, widgetActions, action) { var vm = this; @@ -41,7 +41,7 @@ export default function WidgetActionDialogController($scope, $mdDialog, $filter, vm.actionSourceName = actionSourceName; vm.targetDashboardStateSearchTextChanged = function() { - } + }; vm.dashboardStateSearch = dashboardStateSearch; vm.cancel = cancel; @@ -155,6 +155,12 @@ export default function WidgetActionDialogController($scope, $mdDialog, $filter, case vm.types.widgetActionTypes.custom.value: result.customFunction = action.customFunction; break; + case vm.types.widgetActionTypes.customPretty.value: + result.customResources = action.customResources; + result.customHtml = action.customHtml; + result.customCss = action.customCss; + result.customFunction = action.customFunction; + break; } return result; } diff --git a/ui/src/app/components/widget/action/widget-action-dialog.tpl.html b/ui/src/app/components/widget/action/widget-action-dialog.tpl.html index 09e3120d36..ce182b8288 100644 --- a/ui/src/app/components/widget/action/widget-action-dialog.tpl.html +++ b/ui/src/app/components/widget/action/widget-action-dialog.tpl.html @@ -123,6 +123,7 @@ function-args="{{ ['$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams'] }}" validation-args="{{ [] }}"> + diff --git a/ui/src/app/components/widget/widget.controller.js b/ui/src/app/components/widget/widget.controller.js index 996e808a39..632a6c9e28 100644 --- a/ui/src/app/components/widget/widget.controller.js +++ b/ui/src/app/components/widget/widget.controller.js @@ -17,12 +17,15 @@ import $ from 'jquery'; import 'javascript-detect-element-resize/detect-element-resize'; import Subscription from '../../api/subscription'; +import 'oclazyload'; +import cssjs from '../../../vendor/css.js/css'; + /* eslint-disable angular/angularelement */ /*@ngInject*/ -export default function WidgetController($scope, $state, $timeout, $window, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService, +export default function WidgetController($scope, $state, $timeout, $window, $ocLazyLoad, $element, $q, $log, $injector, $filter, $compile, tbRaf, types, utils, timeService, datasourceService, alarmService, entityService, dashboardService, deviceService, visibleRect, isEdit, isMobile, dashboardTimewindow, - dashboardTimewindowApi, dashboard, widget, aliasController, stateController, widgetInfo, widgetType) { + dashboardTimewindowApi, dashboard, widget, aliasController, stateController, widgetInfo, widgetType, toast) { var vm = this; @@ -38,6 +41,12 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele vm.dashboardTimewindow = dashboardTimewindow; + $window.lazyLoad = $ocLazyLoad; + $window.cssjs = cssjs; + + var cssParser = new cssjs(); + cssParser.testMode = false; + var gridsterItemInited = false; var subscriptionInited = false; var widgetSizeDetected = false; @@ -522,9 +531,95 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele } } break; + case types.widgetActionTypes.customPretty.value: + var customPrettyFunction = descriptor.customFunction; + var customHtml = descriptor.customHtml; + var customCss = descriptor.customCss; + var customResources = descriptor.customResources; + var actionNamespace = 'custom-action-pretty-'+descriptor.name.toLowerCase(); + var htmlTemplate = ''; + if (angular.isDefined(customHtml) && customHtml.length > 0) { + htmlTemplate = customHtml; + } + loadCustomActionResources(actionNamespace, customCss, customResources).then( + function success() { + if (angular.isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { + try { + if (!additionalParams) { + additionalParams = {}; + } + var customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', customPrettyFunction); + customActionPrettyFunction($event, widgetContext, entityId, entityName, htmlTemplate, additionalParams); + } catch (e) { + // + } + } + }, + function fail(errorMessages) { + processResourcesLoadErrors(errorMessages); + } + ); + break; } } + function loadCustomActionResources(actionNamespace, customCss, customResources) { + var deferred = $q.defer(); + + if (angular.isDefined(customCss) && customCss.length > 0) { + cssParser.cssPreviewNamespace = actionNamespace; + cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace'); + } + + function loadNextOrComplete(i) { + i++; + if (i < customResources.length) { + loadNext(i); + } else { + if (errors.length > 0) { + deferred.reject(errors); + } else { + deferred.resolve(); + } + } + } + + function loadNext(i) { + var resourceUrl = customResources[i].url; + if (resourceUrl && resourceUrl.length > 0) { + $ocLazyLoad.load(resourceUrl).then( + function success () { + loadNextOrComplete(i); + }, + function fail() { + errors.push('Failed to load custom action resource: \'' + resourceUrl + '\''); + loadNextOrComplete(i); + } + ); + } else { + loadNextOrComplete(i); + } + } + + if (angular.isDefined(customResources) && customResources.length > 0) { + var errors = []; + loadNext(0); + } else { + deferred.resolve(); + } + + return deferred.promise; + } + + function processResourcesLoadErrors(errorMessages) { + var messageToShow = ''; + for (var e in errorMessages) { + var error = errorMessages[e]; + messageToShow += '
' + error + '
'; + } + toast.showError(messageToShow); + } + function getActiveEntityInfo() { var entityInfo = widgetContext.activeEntityInfo; if (!entityInfo) { diff --git a/ui/src/app/locale/locale.constant-en_US.json b/ui/src/app/locale/locale.constant-en_US.json index 9f4a6ceb87..918b3b4a35 100644 --- a/ui/src/app/locale/locale.constant-en_US.json +++ b/ui/src/app/locale/locale.constant-en_US.json @@ -1512,6 +1512,7 @@ "settings-schema": "Settings schema", "datakey-settings-schema": "Data key settings schema", "javascript": "Javascript", + "js": "JS", "remove-widget-type-title": "Are you sure you want to remove the widget type '{{widgetName}}'?", "remove-widget-type-text": "After the confirmation the widget type and all related data will become unrecoverable.", "remove-widget-type": "Remove widget type", @@ -1528,6 +1529,7 @@ "update-dashboard-state": "Update current dashboard state", "open-dashboard": "Navigate to other dashboard", "custom": "Custom action", + "custom-pretty": "Custom action (with HTML template)", "target-dashboard-state": "Target dashboard state", "target-dashboard-state-required": "Target dashboard state is required", "set-entity-from-widget": "Set entity from widget", diff --git a/ui/src/app/locale/locale.constant-ru_RU.json b/ui/src/app/locale/locale.constant-ru_RU.json index e89ff22f4a..504344c749 100644 --- a/ui/src/app/locale/locale.constant-ru_RU.json +++ b/ui/src/app/locale/locale.constant-ru_RU.json @@ -1464,6 +1464,7 @@ "update-dashboard-state": "Обновить текущее состояние дашборда", "open-dashboard": "Перейти к другому дашборду", "custom": "Пользовательское действие", + "custom-pretty": "Пользовательское действие (с HTML шаблоном)", "target-dashboard-state": "Целевое состояние дашборда", "target-dashboard-state-required": "Целевое состояние дашборда обязательно", "set-entity-from-widget": "Установить объект из виджета", diff --git a/ui/src/app/locale/locale.constant-uk_UA.json b/ui/src/app/locale/locale.constant-uk_UA.json index 03f446d8f3..8c2e9c6def 100644 --- a/ui/src/app/locale/locale.constant-uk_UA.json +++ b/ui/src/app/locale/locale.constant-uk_UA.json @@ -2032,6 +2032,7 @@ "update-dashboard-state": "Оновити поточний стан панелі візуалізації", "open-dashboard": "Перейти до іншої панелі візуалізації", "custom": "Дії користувачів", + "custom-pretty": "Дії користувачів (з HTML шаблоном)", "target-dashboard-state": "Цільовий стан панелі візуалізації", "target-dashboard-state-required": "Необхідно вказати цільовий стан панелі візуалізації", "set-entity-from-widget": "Встановити сутність із віджета",