thingsboard-developers

RuleChain Import/Export

4/2/2018 9:45:48 AM

Details

diff --git a/ui/src/app/api/rule-chain.service.js b/ui/src/app/api/rule-chain.service.js
index af14a3f..b930cf1 100644
--- a/ui/src/app/api/rule-chain.service.js
+++ b/ui/src/app/api/rule-chain.service.js
@@ -253,7 +253,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
         if (ruleChainConnections && ruleChainConnections.length) {
             var tasks = [];
             for (var i = 0; i < ruleChainConnections.length; i++) {
-                tasks.push(getRuleChain(ruleChainConnections[i].targetRuleChainId.id));
+                tasks.push(resolveRuleChain(ruleChainConnections[i].targetRuleChainId.id));
             }
             $q.all(tasks).then(
                 (ruleChains) => {
@@ -273,6 +273,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
         return deferred.promise;
     }
 
+    function resolveRuleChain(ruleChainId) {
+        var deferred = $q.defer();
+        getRuleChain(ruleChainId, {ignoreErrors: true}).then(
+            (ruleChain) => {
+                deferred.resolve(ruleChain);
+            },
+            () => {
+                deferred.resolve({
+                    id: {id: ruleChainId, entityType: types.entityType.rulechain}
+                });
+            }
+        );
+        return deferred.promise;
+    }
+
     function loadRuleNodeComponents() {
         return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes);
     }
diff --git a/ui/src/app/import-export/import-export.service.js b/ui/src/app/import-export/import-export.service.js
index 19c249f..88c6353 100644
--- a/ui/src/app/import-export/import-export.service.js
+++ b/ui/src/app/import-export/import-export.service.js
@@ -281,39 +281,63 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
 
     function exportRuleChain(ruleChainId) {
         ruleChainService.getRuleChain(ruleChainId).then(
-            function success(ruleChain) {
-                var name = ruleChain.name;
-                name = name.toLowerCase().replace(/\W/g,"_");
-                exportToPc(prepareExport(ruleChain), name + '.json');
-                //TODO: metadata
+            (ruleChain) => {
+                ruleChainService.getRuleChainMetaData(ruleChainId).then(
+                    (ruleChainMetaData) => {
+                        var ruleChainExport = {
+                            ruleChain: prepareRuleChain(ruleChain),
+                            metadata: prepareRuleChainMetaData(ruleChainMetaData)
+                        };
+                        var name = ruleChain.name;
+                        name = name.toLowerCase().replace(/\W/g,"_");
+                        exportToPc(ruleChainExport, name + '.json');
+                    },
+                    (rejection) => {
+                        processExportRuleChainRejection(rejection);
+                    }
+                );
             },
-            function fail(rejection) {
-                var message = rejection;
-                if (!message) {
-                    message = $translate.instant('error.unknown-error');
-                }
-                toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
+            (rejection) => {
+                processExportRuleChainRejection(rejection);
             }
         );
     }
 
+    function prepareRuleChain(ruleChain) {
+        ruleChain = prepareExport(ruleChain);
+        if (ruleChain.firstRuleNodeId) {
+            ruleChain.firstRuleNodeId = null;
+        }
+        ruleChain.root = false;
+        return ruleChain;
+    }
+
+    function prepareRuleChainMetaData(ruleChainMetaData) {
+        delete ruleChainMetaData.ruleChainId;
+        for (var i=0;i<ruleChainMetaData.nodes.length;i++) {
+            var node = ruleChainMetaData.nodes[i];
+            ruleChainMetaData.nodes[i] = prepareExport(node);
+        }
+        return ruleChainMetaData;
+    }
+
+    function processExportRuleChainRejection(rejection) {
+        var message = rejection;
+        if (!message) {
+            message = $translate.instant('error.unknown-error');
+        }
+        toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
+    }
+
     function importRuleChain($event) {
         var deferred = $q.defer();
         openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
-            function success(ruleChain) {
-                if (!validateImportedRuleChain(ruleChain)) {
+            function success(ruleChainImport) {
+                if (!validateImportedRuleChain(ruleChainImport)) {
                     toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
                     deferred.reject();
                 } else {
-                    //TODO: rulechain metadata
-                    ruleChainService.saveRuleChain(ruleChain).then(
-                        function success() {
-                            deferred.resolve();
-                        },
-                        function fail() {
-                            deferred.reject();
-                        }
-                    );
+                    deferred.resolve(ruleChainImport);
                 }
             },
             function fail() {
@@ -323,10 +347,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
         return deferred.promise;
     }
 
-    function validateImportedRuleChain(ruleChain) {
-        //TODO: rulechain metadata
-        if (angular.isUndefined(ruleChain.name))
-        {
+    function validateImportedRuleChain(ruleChainImport) {
+        if (angular.isUndefined(ruleChainImport.ruleChain)) {
+            return false;
+        }
+        if (angular.isUndefined(ruleChainImport.metadata)) {
+            return false;
+        }
+        if (angular.isUndefined(ruleChainImport.ruleChain.name)) {
             return false;
         }
         return true;
diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js
index 9c0cd12..f366644 100644
--- a/ui/src/app/locale/locale.constant.js
+++ b/ui/src/app/locale/locale.constant.js
@@ -1175,7 +1175,7 @@ export default angular.module('thingsboard.locale', [])
                     "export": "Export rule chain",
                     "export-failed-error": "Unable to export rule chain: {{error}}",
                     "create-new-rulechain": "Create new rule chain",
-                    "rule-file": "Rule chain file",
+                    "rulechain-file": "Rule chain file",
                     "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
                     "copyId": "Copy rule chain Id",
                     "idCopiedMessage": "Rule chain Id has been copied to clipboard",
@@ -1219,7 +1219,8 @@ export default angular.module('thingsboard.locale', [])
                     "type-rule-chain": "Rule Chain",
                     "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
                     "directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
-                    "ui-resources-load-error": "Failed to load configuration ui resources."
+                    "ui-resources-load-error": "Failed to load configuration ui resources.",
+                    "invalid-target-rulechain": "Unable to resolve target rule chain!"
                 },
                 "rule-plugin": {
                     "management": "Rules and plugins management"
diff --git a/ui/src/app/rulechain/rulechain.controller.js b/ui/src/app/rulechain/rulechain.controller.js
index 14cf798..c20fa44 100644
--- a/ui/src/app/rulechain/rulechain.controller.js
+++ b/ui/src/app/rulechain/rulechain.controller.js
@@ -28,7 +28,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
 /* eslint-enable import/no-unresolved, import/default */
 
 /*@ngInject*/
-export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
+export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $timeout, $mdExpansionPanel, $window, $document, $mdDialog,
                                     $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
                                     ruleChain, ruleChainMetaData, ruleNodeComponents) {
 
@@ -37,6 +37,22 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     vm.$mdExpansionPanel = $mdExpansionPanel;
     vm.types = types;
 
+    if ($state.current.data.import && !ruleChain) {
+        $state.go('home.ruleChains');
+        return;
+    }
+
+    vm.isImport = $state.current.data.import;
+    vm.isConfirmOnExit = false;
+
+    $scope.$watch(function() {
+        return vm.isDirty || vm.isImport;
+    }, (val) => {
+        vm.isConfirmOnExit = val;
+    });
+
+    vm.errorTooltips = {};
+
     vm.isFullscreen = false;
 
     vm.editingRuleNode = null;
@@ -151,6 +167,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         $scope.$broadcast('form-submit');
         if (theForm.$valid) {
             theForm.$setPristine();
+            if (vm.editingRuleNode.error) {
+                delete vm.editingRuleNode.error;
+            }
             vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
             vm.editingRuleNode = angular.copy(vm.editingRuleNode);
             updateRuleNodesHighlight();
@@ -210,7 +229,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         }
         var instances = angular.element.tooltipster.instances();
         instances.forEach((instance) => {
-            instance.destroy();
+            if (!instance.isErrorTooltip) {
+                instance.destroy();
+            }
         });
     }
 
@@ -256,6 +277,71 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
         }, 500);
     }
 
+    function updateNodeErrorTooltip(node) {
+        if (node.error) {
+            var element = angular.element('#' + node.id);
+            var tooltip = vm.errorTooltips[node.id];
+            if (!tooltip || !element.hasClass("tooltipstered")) {
+                element.tooltipster(
+                    {
+                        theme: 'tooltipster-shadow',
+                        delay: 0,
+                        animationDuration: 0,
+                        trigger: 'custom',
+                        triggerOpen: {
+                            click: false,
+                            tap: false
+                        },
+                        triggerClose: {
+                            click: false,
+                            tap: false,
+                            scroll: false
+                        },
+                        side: 'top',
+                        trackOrigin: true
+                    }
+                );
+                var content = '<div class="tb-rule-node-error-tooltip">' +
+                    '<div id="tooltip-content" layout="column">' +
+                    '<div class="tb-node-details">' + node.error + '</div>' +
+                    '</div>' +
+                    '</div>';
+                var contentElement = angular.element(content);
+                $compile(contentElement)($scope);
+                tooltip = element.tooltipster('instance');
+                tooltip.isErrorTooltip = true;
+                tooltip.content(contentElement);
+                vm.errorTooltips[node.id] = tooltip;
+            }
+            $mdUtil.nextTick(() => {
+                tooltip.open();
+            });
+        } else {
+            if (vm.errorTooltips[node.id]) {
+                tooltip = vm.errorTooltips[node.id];
+                tooltip.destroy();
+                delete vm.errorTooltips[node.id];
+            }
+        }
+    }
+
+    function updateErrorTooltips(hide) {
+        for (var nodeId in vm.errorTooltips) {
+            var tooltip = vm.errorTooltips[nodeId];
+            if (hide) {
+                tooltip.close();
+            } else {
+                tooltip.open();
+            }
+        }
+    }
+
+    $scope.$watch(function() {
+        return vm.isEditingRuleNode || vm.isEditingRuleNodeLink;
+    }, (val) => {
+        updateErrorTooltips(val);
+    });
+
     vm.editCallbacks = {
         edgeDoubleClick: function (event, edge) {
             var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
@@ -381,6 +467,16 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                     vm.ruleNodeTypesCanvasControl[componentType].adjustCanvasSize(true);
                 }
             }
+            for (componentType in vm.ruleNodeTypesModel) {
+                var panel = vm.$mdExpansionPanel(componentType);
+                if (panel) {
+                    if (!vm.ruleNodeTypesModel[componentType].model.nodes.length) {
+                        panel.collapse();
+                    } else {
+                        panel.expand();
+                    }
+                }
+            }
         });
     }
 
@@ -512,11 +608,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                         ruleChainNode = {
                             id: 'rule-chain-node-' + vm.nextNodeID++,
                             additionalInfo: ruleChainConnection.additionalInfo,
-                            targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
                             x: ruleChainConnection.additionalInfo.layoutX,
                             y: ruleChainConnection.additionalInfo.layoutY,
                             component: types.ruleChainNodeComponent,
-                            name: ruleChain.name,
                             nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
                             icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
                             connectors: [
@@ -526,6 +620,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
                                 }
                             ]
                         };
+                        if (ruleChain.name) {
+                            ruleChainNode.name = ruleChain.name;
+                            ruleChainNode.targetRuleChainId = ruleChainConnection.targetRuleChainId.id;
+                        } else {
+                            ruleChainNode.name = "Unresolved";
+                            ruleChainNode.targetRuleChainId = null;
+                            ruleChainNode.error = $translate.instant('rulenode.invalid-target-rulechain');
+                        }
                         ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
                         vm.ruleChainModel.nodes.push(ruleChainNode);
                     }
@@ -553,11 +655,16 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
 
         updateRuleNodesHighlight();
 
+        validate();
+
         $mdUtil.nextTick(() => {
             vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
                 function (newVal, oldVal) {
-                    if (!vm.isDirty && !angular.equals(newVal, oldVal)) {
-                        vm.isDirty = true;
+                    if (!angular.equals(newVal, oldVal)) {
+                        validate();
+                        if (!vm.isDirty) {
+                            vm.isDirty = true;
+                        }
                     }
                 }, true
             );
@@ -565,91 +672,122 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
     }
 
     function updateRuleNodesHighlight() {
-        for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
+        for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
             vm.ruleChainModel.nodes[i].highlighted = false;
         }
         if ($scope.searchConfig.searchText) {
             var res = $filter('filter')(vm.ruleChainModel.nodes, {name: $scope.searchConfig.searchText});
             if (res) {
-                for (i=0;i<res.length;i++) {
+                for (i = 0; i < res.length; i++) {
                     res[i].highlighted = true;
                 }
             }
         }
     }
 
+    function validate() {
+        $mdUtil.nextTick(() => {
+            vm.isInvalid = false;
+            for (var i = 0; i < vm.ruleChainModel.nodes.length; i++) {
+                if (vm.ruleChainModel.nodes[i].error) {
+                    vm.isInvalid = true;
+                }
+                updateNodeErrorTooltip(vm.ruleChainModel.nodes[i]);
+            }
+        });
+    }
+
     function saveRuleChain() {
-        var ruleChainMetaData = {
-            ruleChainId: vm.ruleChain.id,
-            nodes: [],
-            connections: [],
-            ruleChainConnections: []
-        };
+        var saveRuleChainPromise;
+        if (vm.isImport) {
+            saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain);
+        } else {
+            saveRuleChainPromise = $q.when(vm.ruleChain);
+        }
+        saveRuleChainPromise.then(
+            (ruleChain) => {
+                vm.ruleChain = ruleChain;
+                var ruleChainMetaData = {
+                    ruleChainId: vm.ruleChain.id,
+                    nodes: [],
+                    connections: [],
+                    ruleChainConnections: []
+                };
 
-        var nodes = [];
+                var nodes = [];
 
-        for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
-            var node = vm.ruleChainModel.nodes[i];
-            if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) {
-                var ruleNode = {};
-                if (node.ruleNodeId) {
-                    ruleNode.id = node.ruleNodeId;
+                for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
+                    var node = vm.ruleChainModel.nodes[i];
+                    if (node.component.type != types.ruleNodeType.INPUT.value && node.component.type != types.ruleNodeType.RULE_CHAIN.value) {
+                        var ruleNode = {};
+                        if (node.ruleNodeId) {
+                            ruleNode.id = node.ruleNodeId;
+                        }
+                        ruleNode.type = node.component.clazz;
+                        ruleNode.name = node.name;
+                        ruleNode.configuration = node.configuration;
+                        ruleNode.additionalInfo = node.additionalInfo;
+                        ruleNode.debugMode = node.debugMode;
+                        if (!ruleNode.additionalInfo) {
+                            ruleNode.additionalInfo = {};
+                        }
+                        ruleNode.additionalInfo.layoutX = node.x;
+                        ruleNode.additionalInfo.layoutY = node.y;
+                        ruleChainMetaData.nodes.push(ruleNode);
+                        nodes.push(node);
+                    }
                 }
-                ruleNode.type = node.component.clazz;
-                ruleNode.name = node.name;
-                ruleNode.configuration = node.configuration;
-                ruleNode.additionalInfo = node.additionalInfo;
-                ruleNode.debugMode = node.debugMode;
-                if (!ruleNode.additionalInfo) {
-                    ruleNode.additionalInfo = {};
+                var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
+                if (res && res.length) {
+                    var firstNodeEdge = res[0];
+                    var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
+                    ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
                 }
-                ruleNode.additionalInfo.layoutX = node.x;
-                ruleNode.additionalInfo.layoutY = node.y;
-                ruleChainMetaData.nodes.push(ruleNode);
-                nodes.push(node);
-            }
-        }
-        var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
-        if (res && res.length) {
-            var firstNodeEdge = res[0];
-            var firstNode = vm.modelservice.nodes.getNodeByConnectorId(firstNodeEdge.destination);
-            ruleChainMetaData.firstNodeIndex = nodes.indexOf(firstNode);
-        }
-        for (i=0;i<vm.ruleChainModel.edges.length;i++) {
-            var edge = vm.ruleChainModel.edges[i];
-            var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
-            var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
-            if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
-                var fromIndex = nodes.indexOf(sourceNode);
-                if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
-                    var ruleChainConnection = {
-                        fromIndex: fromIndex,
-                        targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
-                        additionalInfo: destNode.additionalInfo,
-                        type: edge.label
-                    };
-                    if (!ruleChainConnection.additionalInfo) {
-                        ruleChainConnection.additionalInfo = {};
+                for (i=0;i<vm.ruleChainModel.edges.length;i++) {
+                    var edge = vm.ruleChainModel.edges[i];
+                    var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
+                    var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
+                    if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
+                        var fromIndex = nodes.indexOf(sourceNode);
+                        if (destNode.component.type == types.ruleNodeType.RULE_CHAIN.value) {
+                            var ruleChainConnection = {
+                                fromIndex: fromIndex,
+                                targetRuleChainId: {entityType: vm.types.entityType.rulechain, id: destNode.targetRuleChainId},
+                                additionalInfo: destNode.additionalInfo,
+                                type: edge.label
+                            };
+                            if (!ruleChainConnection.additionalInfo) {
+                                ruleChainConnection.additionalInfo = {};
+                            }
+                            ruleChainConnection.additionalInfo.layoutX = destNode.x;
+                            ruleChainConnection.additionalInfo.layoutY = destNode.y;
+                            ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
+                            ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
+                        } else {
+                            var toIndex = nodes.indexOf(destNode);
+                            var nodeConnection = {
+                                fromIndex: fromIndex,
+                                toIndex: toIndex,
+                                type: edge.label
+                            };
+                            ruleChainMetaData.connections.push(nodeConnection);
+                        }
                     }
-                    ruleChainConnection.additionalInfo.layoutX = destNode.x;
-                    ruleChainConnection.additionalInfo.layoutY = destNode.y;
-                    ruleChainConnection.additionalInfo.ruleChainNodeId = destNode.id;
-                    ruleChainMetaData.ruleChainConnections.push(ruleChainConnection);
-                } else {
-                    var toIndex = nodes.indexOf(destNode);
-                    var nodeConnection = {
-                        fromIndex: fromIndex,
-                        toIndex: toIndex,
-                        type: edge.label
-                    };
-                    ruleChainMetaData.connections.push(nodeConnection);
                 }
-            }
-        }
-        ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
-            (ruleChainMetaData) => {
-                vm.ruleChainMetaData = ruleChainMetaData;
-                prepareRuleChain();
+                ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
+                    (ruleChainMetaData) => {
+                        vm.ruleChainMetaData = ruleChainMetaData;
+                        if (vm.isImport) {
+                            vm.isDirty = false;
+                            vm.isImport = false;
+                            $mdUtil.nextTick(() => {
+                                $state.go('home.ruleChains.ruleChain', {ruleChainId: vm.ruleChain.id.id});
+                            });
+                        } else {
+                            prepareRuleChain();
+                        }
+                    }
+                );
             }
         );
     }
@@ -662,12 +800,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
 
         ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
 
+        var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null;
+
         $mdDialog.show({
             controller: 'AddRuleNodeController',
             controllerAs: 'vm',
             templateUrl: addRuleNodeTemplate,
             parent: angular.element($document[0].body),
-            locals: {ruleNode: ruleNode, ruleChainId: vm.ruleChain.id.id},
+            locals: {ruleNode: ruleNode, ruleChainId: ruleChainId},
             fullscreen: true,
             targetEvent: $event
         }).then(function (ruleNode) {
diff --git a/ui/src/app/rulechain/rulechain.routes.js b/ui/src/app/rulechain/rulechain.routes.js
index 48559d9..2aefd82 100644
--- a/ui/src/app/rulechain/rulechain.routes.js
+++ b/ui/src/app/rulechain/rulechain.routes.js
@@ -76,11 +76,52 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
                     }
             },
             data: {
+                import: false,
                 searchEnabled: true,
                 pageTitle: 'rulechain.rulechain'
             },
             ncyBreadcrumb: {
                 label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}'
             }
+    }).state('home.ruleChains.importRuleChain', {
+        url: '/ruleChain/import',
+        reloadOnSearch: false,
+        module: 'private',
+        auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
+        views: {
+            "content@home": {
+                templateUrl: ruleChainTemplate,
+                controller: 'RuleChainController',
+                controllerAs: 'vm'
+            }
+        },
+        params: {
+            ruleChainImport: {}
+        },
+        resolve: {
+            ruleChain:
+            /*@ngInject*/
+                function($stateParams) {
+                    return $stateParams.ruleChainImport.ruleChain;
+                },
+            ruleChainMetaData:
+            /*@ngInject*/
+                function($stateParams) {
+                    return $stateParams.ruleChainImport.metadata;
+                },
+            ruleNodeComponents:
+            /*@ngInject*/
+                function($stateParams, ruleChainService) {
+                    return ruleChainService.getRuleNodeComponents();
+                }
+        },
+        data: {
+            import: true,
+            searchEnabled: true,
+            pageTitle: 'rulechain.rulechain'
+        },
+        ncyBreadcrumb: {
+            label: '{"icon": "settings_ethernet", "label": "{{ (\'rulechain.import\' | translate) + \': \'+ vm.ruleChain.name }}", "translate": "false"}'
+        }
     });
 }
diff --git a/ui/src/app/rulechain/rulechain.scss b/ui/src/app/rulechain/rulechain.scss
index 850de3c..187a722 100644
--- a/ui/src/app/rulechain/rulechain.scss
+++ b/ui/src/app/rulechain/rulechain.scss
@@ -125,13 +125,16 @@
   color: #333;
   border: solid 1px #777;
   font-size: 12px;
-  &.tb-rule-node-highlighted {
+  &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
     box-shadow: 0 0 10px 6px #51cbee;
     .tb-node-title {
       text-decoration: underline;
       font-weight: bold;
     }
   }
+  &.tb-rule-node-invalid {
+    box-shadow: 0 0 10px 6px #ff5c50;
+  }
   &.tb-input-type {
     background-color: #a3eaa9;
     user-select: none;
@@ -191,10 +194,6 @@
     bottom: 0;
     background-color: #000;
     opacity: 0;
-/*    &.tb-rule-node-highlighted {
-      background-color: green;
-      opacity: 0.15;
-    }*/
   }
   &.fc-hover {
     .fc-node-overlay {
@@ -391,6 +390,14 @@
   font-size: 14px;
   width: 300px;
   color: #333;
+}
+
+.tb-rule-node-error-tooltip {
+  font-size: 16px;
+  color: #ea0d0d;
+}
+
+.tb-rule-node-tooltip, .tb-rule-node-error-tooltip {
   #tooltip-content {
     .tb-node-title {
       font-weight: 600;
diff --git a/ui/src/app/rulechain/rulechain.tpl.html b/ui/src/app/rulechain/rulechain.tpl.html
index a843b03..37fcd9b 100644
--- a/ui/src/app/rulechain/rulechain.tpl.html
+++ b/ui/src/app/rulechain/rulechain.tpl.html
@@ -16,7 +16,7 @@
 
 -->
 
-<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isDirty"
+<md-content flex tb-expand-fullscreen tb-confirm-on-exit is-dirty="vm.isConfirmOnExit"
             expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
             ng-keydown="vm.keyDown($event)"
             ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded">
@@ -185,7 +185,7 @@
             </md-tooltip>
             <ng-md-icon icon="delete"></ng-md-icon>
         </md-button>
-        <md-button ng-disabled="$root.loading || !vm.isDirty"
+        <md-button ng-disabled="$root.loading  || vm.isInvalid || (!vm.isDirty && !vm.isImport)"
                    class="tb-btn-footer md-accent md-hue-2 md-fab"
                    aria-label="{{ 'action.apply' | translate }}"
                    ng-click="vm.saveRuleChain()">
diff --git a/ui/src/app/rulechain/rulechains.controller.js b/ui/src/app/rulechain/rulechains.controller.js
index 2a30b0c..7c857f2 100644
--- a/ui/src/app/rulechain/rulechains.controller.js
+++ b/ui/src/app/rulechain/rulechains.controller.js
@@ -63,8 +63,8 @@ export default function RuleChainsController(ruleChainService, userService, impo
         {
             onAction: function ($event) {
                 importExport.importRuleChain($event).then(
-                    function() {
-                        vm.grid.refreshList();
+                    function(ruleChainImport) {
+                        $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport});
                     }
                 );
             },
diff --git a/ui/src/app/rulechain/rulenode.tpl.html b/ui/src/app/rulechain/rulenode.tpl.html
index 83ce988..973ea1f 100644
--- a/ui/src/app/rulechain/rulenode.tpl.html
+++ b/ui/src/app/rulechain/rulenode.tpl.html
@@ -22,8 +22,8 @@
         ng-mousedown="callbacks.mouseDown($event, node)"
         ng-mouseenter="callbacks.mouseEnter($event, node)"
         ng-mouseleave="callbacks.mouseLeave($event, node)">
-    <div class="{{flowchartConstants.nodeOverlayClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted}"></div>
-    <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted}">
+    <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
+    <div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted, 'tb-rule-node-invalid': node.error }">
         <md-icon aria-label="node-type-icon" flex="15"
                  class="material-icons">{{node.icon}}</md-icon>
         <div layout="column" flex="85" layout-align="center">