RuleChain Import/Export

This commit is contained in:
Igor Kulikov 2018-04-02 15:45:48 +03:00
parent dc47727cf5
commit 9c0f6e9925
9 changed files with 349 additions and 117 deletions

View File

@ -253,7 +253,7 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
if (ruleChainConnections && ruleChainConnections.length) { if (ruleChainConnections && ruleChainConnections.length) {
var tasks = []; var tasks = [];
for (var i = 0; i < ruleChainConnections.length; i++) { 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( $q.all(tasks).then(
(ruleChains) => { (ruleChains) => {
@ -273,6 +273,21 @@ function RuleChainService($http, $q, $filter, $ocLazyLoad, $translate, types, co
return deferred.promise; 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() { function loadRuleNodeComponents() {
return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes); return componentDescriptorService.getComponentDescriptorsByTypes(types.ruleNodeTypeComponentTypes);
} }

View File

@ -281,39 +281,63 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
function exportRuleChain(ruleChainId) { function exportRuleChain(ruleChainId) {
ruleChainService.getRuleChain(ruleChainId).then( ruleChainService.getRuleChain(ruleChainId).then(
function success(ruleChain) { (ruleChain) => {
ruleChainService.getRuleChainMetaData(ruleChainId).then(
(ruleChainMetaData) => {
var ruleChainExport = {
ruleChain: prepareRuleChain(ruleChain),
metadata: prepareRuleChainMetaData(ruleChainMetaData)
};
var name = ruleChain.name; var name = ruleChain.name;
name = name.toLowerCase().replace(/\W/g,"_"); name = name.toLowerCase().replace(/\W/g,"_");
exportToPc(prepareExport(ruleChain), name + '.json'); exportToPc(ruleChainExport, name + '.json');
//TODO: metadata
}, },
function fail(rejection) { (rejection) => {
processExportRuleChainRejection(rejection);
}
);
},
(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; var message = rejection;
if (!message) { if (!message) {
message = $translate.instant('error.unknown-error'); message = $translate.instant('error.unknown-error');
} }
toast.showError($translate.instant('rulechain.export-failed-error', {error: message})); toast.showError($translate.instant('rulechain.export-failed-error', {error: message}));
} }
);
}
function importRuleChain($event) { function importRuleChain($event) {
var deferred = $q.defer(); var deferred = $q.defer();
openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then( openImportDialog($event, 'rulechain.import', 'rulechain.rulechain-file').then(
function success(ruleChain) { function success(ruleChainImport) {
if (!validateImportedRuleChain(ruleChain)) { if (!validateImportedRuleChain(ruleChainImport)) {
toast.showError($translate.instant('rulechain.invalid-rulechain-file-error')); toast.showError($translate.instant('rulechain.invalid-rulechain-file-error'));
deferred.reject(); deferred.reject();
} else { } else {
//TODO: rulechain metadata deferred.resolve(ruleChainImport);
ruleChainService.saveRuleChain(ruleChain).then(
function success() {
deferred.resolve();
},
function fail() {
deferred.reject();
}
);
} }
}, },
function fail() { function fail() {
@ -323,10 +347,14 @@ export default function ImportExport($log, $translate, $q, $mdDialog, $document,
return deferred.promise; return deferred.promise;
} }
function validateImportedRuleChain(ruleChain) { function validateImportedRuleChain(ruleChainImport) {
//TODO: rulechain metadata if (angular.isUndefined(ruleChainImport.ruleChain)) {
if (angular.isUndefined(ruleChain.name)) return false;
{ }
if (angular.isUndefined(ruleChainImport.metadata)) {
return false;
}
if (angular.isUndefined(ruleChainImport.ruleChain.name)) {
return false; return false;
} }
return true; return true;

View File

@ -1175,7 +1175,7 @@ export default angular.module('thingsboard.locale', [])
"export": "Export rule chain", "export": "Export rule chain",
"export-failed-error": "Unable to export rule chain: {{error}}", "export-failed-error": "Unable to export rule chain: {{error}}",
"create-new-rulechain": "Create new rule chain", "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.", "invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
"copyId": "Copy rule chain Id", "copyId": "Copy rule chain Id",
"idCopiedMessage": "Rule chain Id has been copied to clipboard", "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": "Rule Chain",
"type-rule-chain-details": "Forwards incoming messages to specified Rule Chain", "type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
"directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.", "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": { "rule-plugin": {
"management": "Rules and plugins management" "management": "Rules and plugins management"

View File

@ -28,7 +28,7 @@ import addRuleNodeLinkTemplate from './add-link.tpl.html';
/* eslint-enable import/no-unresolved, import/default */ /* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/ /*@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, $filter, $translate, hotkeys, types, ruleChainService, Modelfactory, flowchartConstants,
ruleChain, ruleChainMetaData, ruleNodeComponents) { ruleChain, ruleChainMetaData, ruleNodeComponents) {
@ -37,6 +37,22 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
vm.$mdExpansionPanel = $mdExpansionPanel; vm.$mdExpansionPanel = $mdExpansionPanel;
vm.types = types; 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.isFullscreen = false;
vm.editingRuleNode = null; vm.editingRuleNode = null;
@ -151,6 +167,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
$scope.$broadcast('form-submit'); $scope.$broadcast('form-submit');
if (theForm.$valid) { if (theForm.$valid) {
theForm.$setPristine(); theForm.$setPristine();
if (vm.editingRuleNode.error) {
delete vm.editingRuleNode.error;
}
vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode; vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
vm.editingRuleNode = angular.copy(vm.editingRuleNode); vm.editingRuleNode = angular.copy(vm.editingRuleNode);
updateRuleNodesHighlight(); updateRuleNodesHighlight();
@ -210,7 +229,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
} }
var instances = angular.element.tooltipster.instances(); var instances = angular.element.tooltipster.instances();
instances.forEach((instance) => { instances.forEach((instance) => {
if (!instance.isErrorTooltip) {
instance.destroy(); instance.destroy();
}
}); });
} }
@ -256,6 +277,71 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
}, 500); }, 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 = { vm.editCallbacks = {
edgeDoubleClick: function (event, edge) { edgeDoubleClick: function (event, edge) {
var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); 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); 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 = { ruleChainNode = {
id: 'rule-chain-node-' + vm.nextNodeID++, id: 'rule-chain-node-' + vm.nextNodeID++,
additionalInfo: ruleChainConnection.additionalInfo, additionalInfo: ruleChainConnection.additionalInfo,
targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
x: ruleChainConnection.additionalInfo.layoutX, x: ruleChainConnection.additionalInfo.layoutX,
y: ruleChainConnection.additionalInfo.layoutY, y: ruleChainConnection.additionalInfo.layoutY,
component: types.ruleChainNodeComponent, component: types.ruleChainNodeComponent,
name: ruleChain.name,
nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass, nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
icon: vm.types.ruleNodeType.RULE_CHAIN.icon, icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
connectors: [ 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; ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
vm.ruleChainModel.nodes.push(ruleChainNode); vm.ruleChainModel.nodes.push(ruleChainNode);
} }
@ -553,12 +655,17 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
updateRuleNodesHighlight(); updateRuleNodesHighlight();
validate();
$mdUtil.nextTick(() => { $mdUtil.nextTick(() => {
vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel', vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
function (newVal, oldVal) { function (newVal, oldVal) {
if (!vm.isDirty && !angular.equals(newVal, oldVal)) { if (!angular.equals(newVal, oldVal)) {
validate();
if (!vm.isDirty) {
vm.isDirty = true; vm.isDirty = true;
} }
}
}, true }, true
); );
}); });
@ -578,7 +685,28 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
} }
} }
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() { function saveRuleChain() {
var saveRuleChainPromise;
if (vm.isImport) {
saveRuleChainPromise = ruleChainService.saveRuleChain(vm.ruleChain);
} else {
saveRuleChainPromise = $q.when(vm.ruleChain);
}
saveRuleChainPromise.then(
(ruleChain) => {
vm.ruleChain = ruleChain;
var ruleChainMetaData = { var ruleChainMetaData = {
ruleChainId: vm.ruleChain.id, ruleChainId: vm.ruleChain.id,
nodes: [], nodes: [],
@ -649,8 +777,18 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then( ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
(ruleChainMetaData) => { (ruleChainMetaData) => {
vm.ruleChainMetaData = 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(); prepareRuleChain();
} }
}
);
}
); );
} }
@ -662,12 +800,14 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration); ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
var ruleChainId = vm.ruleChain.id ? vm.ruleChain.id.id : null;
$mdDialog.show({ $mdDialog.show({
controller: 'AddRuleNodeController', controller: 'AddRuleNodeController',
controllerAs: 'vm', controllerAs: 'vm',
templateUrl: addRuleNodeTemplate, templateUrl: addRuleNodeTemplate,
parent: angular.element($document[0].body), parent: angular.element($document[0].body),
locals: {ruleNode: ruleNode, ruleChainId: vm.ruleChain.id.id}, locals: {ruleNode: ruleNode, ruleChainId: ruleChainId},
fullscreen: true, fullscreen: true,
targetEvent: $event targetEvent: $event
}).then(function (ruleNode) { }).then(function (ruleNode) {

View File

@ -76,11 +76,52 @@ export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider
} }
}, },
data: { data: {
import: false,
searchEnabled: true, searchEnabled: true,
pageTitle: 'rulechain.rulechain' pageTitle: 'rulechain.rulechain'
}, },
ncyBreadcrumb: { ncyBreadcrumb: {
label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}' 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"}'
}
}); });
} }

View File

@ -125,13 +125,16 @@
color: #333; color: #333;
border: solid 1px #777; border: solid 1px #777;
font-size: 12px; font-size: 12px;
&.tb-rule-node-highlighted { &.tb-rule-node-highlighted:not(.tb-rule-node-invalid) {
box-shadow: 0 0 10px 6px #51cbee; box-shadow: 0 0 10px 6px #51cbee;
.tb-node-title { .tb-node-title {
text-decoration: underline; text-decoration: underline;
font-weight: bold; font-weight: bold;
} }
} }
&.tb-rule-node-invalid {
box-shadow: 0 0 10px 6px #ff5c50;
}
&.tb-input-type { &.tb-input-type {
background-color: #a3eaa9; background-color: #a3eaa9;
user-select: none; user-select: none;
@ -191,10 +194,6 @@
bottom: 0; bottom: 0;
background-color: #000; background-color: #000;
opacity: 0; opacity: 0;
/* &.tb-rule-node-highlighted {
background-color: green;
opacity: 0.15;
}*/
} }
&.fc-hover { &.fc-hover {
.fc-node-overlay { .fc-node-overlay {
@ -391,6 +390,14 @@
font-size: 14px; font-size: 14px;
width: 300px; width: 300px;
color: #333; color: #333;
}
.tb-rule-node-error-tooltip {
font-size: 16px;
color: #ea0d0d;
}
.tb-rule-node-tooltip, .tb-rule-node-error-tooltip {
#tooltip-content { #tooltip-content {
.tb-node-title { .tb-node-title {
font-weight: 600; font-weight: 600;

View File

@ -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" expand-tooltip-direction="bottom" layout="column" class="tb-rulechain"
ng-keydown="vm.keyDown($event)" ng-keydown="vm.keyDown($event)"
ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded"> ng-keyup="vm.keyUp($event)" on-fullscreen-changed="vm.isFullscreen = expanded">
@ -185,7 +185,7 @@
</md-tooltip> </md-tooltip>
<ng-md-icon icon="delete"></ng-md-icon> <ng-md-icon icon="delete"></ng-md-icon>
</md-button> </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" class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.apply' | translate }}" aria-label="{{ 'action.apply' | translate }}"
ng-click="vm.saveRuleChain()"> ng-click="vm.saveRuleChain()">

View File

@ -63,8 +63,8 @@ export default function RuleChainsController(ruleChainService, userService, impo
{ {
onAction: function ($event) { onAction: function ($event) {
importExport.importRuleChain($event).then( importExport.importRuleChain($event).then(
function() { function(ruleChainImport) {
vm.grid.refreshList(); $state.go('home.ruleChains.importRuleChain', {ruleChainImport:ruleChainImport});
} }
); );
}, },

View File

@ -22,8 +22,8 @@
ng-mousedown="callbacks.mouseDown($event, node)" ng-mousedown="callbacks.mouseDown($event, node)"
ng-mouseenter="callbacks.mouseEnter($event, node)" ng-mouseenter="callbacks.mouseEnter($event, node)"
ng-mouseleave="callbacks.mouseLeave($event, node)"> ng-mouseleave="callbacks.mouseLeave($event, node)">
<div class="{{flowchartConstants.nodeOverlayClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted}"></div> <div class="{{flowchartConstants.nodeOverlayClass}}"></div>
<div class="tb-rule-node {{node.nodeClass}}" ng-class="{'tb-rule-node-highlighted' : node.highlighted}"> <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" <md-icon aria-label="node-type-icon" flex="15"
class="material-icons">{{node.icon}}</md-icon> class="material-icons">{{node.icon}}</md-icon>
<div layout="column" flex="85" layout-align="center"> <div layout="column" flex="85" layout-align="center">