Browse Source

Rule chains UI

pull/725/head
Igor Kulikov 9 years ago
parent
commit
6ece02f88f
  1. 9
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java
  2. 4
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  3. 1
      ui/package.json
  4. 8
      ui/src/app/api/entity.service.js
  5. 137
      ui/src/app/api/rule-chain.service.js
  6. 3
      ui/src/app/app.js
  7. 38
      ui/src/app/common/types.constant.js
  8. 6
      ui/src/app/entity/entity-autocomplete.directive.js
  9. 20
      ui/src/app/locale/locale.constant.js
  10. 48
      ui/src/app/rulechain/add-link.tpl.html
  11. 48
      ui/src/app/rulechain/add-rulenode.tpl.html
  12. 10
      ui/src/app/rulechain/index.js
  13. 39
      ui/src/app/rulechain/link-fieldset.tpl.html
  14. 71
      ui/src/app/rulechain/link.directive.js
  15. 2
      ui/src/app/rulechain/rulechain-fieldset.tpl.html
  16. 638
      ui/src/app/rulechain/rulechain.controller.js
  17. 41
      ui/src/app/rulechain/rulechain.routes.js
  18. 262
      ui/src/app/rulechain/rulechain.scss
  19. 130
      ui/src/app/rulechain/rulechain.tpl.html
  20. 188
      ui/src/app/rulechain/rulechains.controller.js
  21. 55
      ui/src/app/rulechain/rulenode-fieldset.tpl.html
  22. 79
      ui/src/app/rulechain/rulenode.directive.js
  23. 45
      ui/src/app/rulechain/rulenode.tpl.html

9
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleChainMetaData.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.rule;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import org.thingsboard.server.common.data.id.RuleChainId;
@ -47,11 +48,12 @@ public class RuleChainMetaData {
}
connections.add(connectionInfo);
}
public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type) {
public void addRuleChainConnectionInfo(int fromIndex, RuleChainId targetRuleChainId, String type, JsonNode additionalInfo) {
RuleChainConnectionInfo connectionInfo = new RuleChainConnectionInfo();
connectionInfo.setFromIndex(fromIndex);
connectionInfo.setTargetRuleChainId(targetRuleChainId);
connectionInfo.setType(type);
connectionInfo.setAdditionalInfo(additionalInfo);
if (ruleChainConnections == null) {
ruleChainConnections = new ArrayList<>();
}
@ -59,16 +61,17 @@ public class RuleChainMetaData {
}
@Data
public class NodeConnectionInfo {
public static class NodeConnectionInfo {
private int fromIndex;
private int toIndex;
private String type;
}
@Data
public class RuleChainConnectionInfo {
public static class RuleChainConnectionInfo {
private int fromIndex;
private RuleChainId targetRuleChainId;
private JsonNode additionalInfo;
private String type;
}

4
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -166,7 +166,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
EntityId to = nodeToRuleChainConnection.getTargetRuleChainId();
String type = nodeToRuleChainConnection.getType();
try {
createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE));
createRelation(new EntityRelation(from, to, type, RelationTypeGroup.RULE_NODE, nodeToRuleChainConnection.getAdditionalInfo()));
} catch (ExecutionException | InterruptedException e) {
log.warn("[{}] Failed to create rule node to rule chain relation. from: [{}], to: [{}]", from, to);
throw new RuntimeException(e);
@ -206,7 +206,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
ruleChainMetaData.addConnectionInfo(fromIndex, toIndex, type);
} else if (nodeRelation.getTo().getEntityType() == EntityType.RULE_CHAIN) {
RuleChainId targetRuleChainId = new RuleChainId(nodeRelation.getTo().getId());
ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type);
ruleChainMetaData.addRuleChainConnectionInfo(fromIndex, targetRuleChainId, type, nodeRelation.getAdditionalInfo());
}
}
}

1
ui/package.json

@ -69,6 +69,7 @@
"moment": "^2.15.0",
"ngclipboard": "^1.1.1",
"ngreact": "^0.3.0",
"ngFlowchart": "git://github.com/ikulikov/ngFlowchart.git#master",
"objectpath": "^1.2.1",
"oclazyload": "^1.0.9",
"raphael": "^2.2.7",

8
ui/src/app/api/entity.service.js

@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.entity', [thingsboardTypes])
/*@ngInject*/
function EntityService($http, $q, $filter, $translate, $log, userService, deviceService,
assetService, tenantService, customerService,
ruleService, pluginService, dashboardService, entityRelationService, attributeService, types, utils) {
ruleService, pluginService, ruleChainService, dashboardService, entityRelationService, attributeService, types, utils) {
var service = {
getEntity: getEntity,
getEntities: getEntities,
@ -73,6 +73,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.user:
promise = userService.getUser(entityId, true, config);
break;
case types.entityType.rulechain:
promise = ruleChainService.getRuleChain(entityId, config);
break;
case types.entityType.alarm:
$log.error('Get Alarm Entity is not implemented!');
break;
@ -271,6 +274,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device
case types.entityType.plugin:
promise = pluginService.getAllPlugins(pageLink, config);
break;
case types.entityType.rulechain:
promise = ruleChainService.getRuleChains(pageLink, config);
break;
case types.entityType.dashboard:
if (user.authority === 'CUSTOMER_USER') {
promise = dashboardService.getCustomerDashboards(customerId, pageLink, config);

137
ui/src/app/api/rule-chain.service.js

@ -17,7 +17,9 @@ export default angular.module('thingsboard.api.ruleChain', [])
.factory('ruleChainService', RuleChainService).name;
/*@ngInject*/
function RuleChainService($http, $q) {
function RuleChainService($http, $q, $filter, types) {
var ruleNodeTypes = null;
var service = {
getSystemRuleChains: getSystemRuleChains,
@ -27,7 +29,11 @@ function RuleChainService($http, $q) {
saveRuleChain: saveRuleChain,
deleteRuleChain: deleteRuleChain,
getRuleChainMetaData: getRuleChainMetaData,
saveRuleChainMetaData: saveRuleChainMetaData
saveRuleChainMetaData: saveRuleChainMetaData,
getRuleNodeTypes: getRuleNodeTypes,
getRuleNodeComponentType: getRuleNodeComponentType,
getRuleNodeSupportedLinks: getRuleNodeSupportedLinks,
resolveTargetRuleChains: resolveTargetRuleChains
};
return service;
@ -147,4 +153,131 @@ function RuleChainService($http, $q) {
return deferred.promise;
}
function getRuleNodeSupportedLinks(nodeType) { //eslint-disable-line
//TODO:
var deferred = $q.defer();
var linkLabels = [
{ name: 'Success', custom: false },
{ name: 'Fail', custom: false },
{ name: 'Custom', custom: true },
];
deferred.resolve(linkLabels);
return deferred.promise;
}
function getRuleNodeTypes() {
var deferred = $q.defer();
if (ruleNodeTypes) {
deferred.resolve(ruleNodeTypes);
} else {
loadRuleNodeTypes().then(
(nodeTypes) => {
ruleNodeTypes = nodeTypes;
ruleNodeTypes.push(
{
nodeType: types.ruleNodeType.RULE_CHAIN.value,
type: 'Rule chain'
}
);
deferred.resolve(ruleNodeTypes);
},
() => {
deferred.reject();
}
);
}
return deferred.promise;
}
function getRuleNodeComponentType(type) {
var res = $filter('filter')(ruleNodeTypes, {type: type}, true);
if (res && res.length) {
return res[0].nodeType;
}
return null;
}
function resolveTargetRuleChains(ruleChainConnections) {
var deferred = $q.defer();
if (ruleChainConnections && ruleChainConnections.length) {
var tasks = [];
for (var i = 0; i < ruleChainConnections.length; i++) {
tasks.push(getRuleChain(ruleChainConnections[i].targetRuleChainId.id));
}
$q.all(tasks).then(
(ruleChains) => {
var ruleChainsMap = {};
for (var i = 0; i < ruleChains.length; i++) {
ruleChainsMap[ruleChains[i].id.id] = ruleChains[i];
}
deferred.resolve(ruleChainsMap);
},
() => {
deferred.reject();
}
);
} else {
deferred.resolve({});
}
return deferred.promise;
}
function loadRuleNodeTypes() {
var deferred = $q.defer();
deferred.resolve(
[
{
nodeType: types.ruleNodeType.FILTER.value,
type: 'Filter'
},
{
nodeType: types.ruleNodeType.FILTER.value,
type: 'Switch'
},
{
nodeType: types.ruleNodeType.ENRICHMENT.value,
type: 'Self'
},
{
nodeType: types.ruleNodeType.ENRICHMENT.value,
type: 'Tenant/Customer'
},
{
nodeType: types.ruleNodeType.ENRICHMENT.value,
type: 'Related Entity'
},
{
nodeType: types.ruleNodeType.ENRICHMENT.value,
type: 'Last Telemetry'
},
{
nodeType: types.ruleNodeType.TRANSFORMATION.value,
type: 'Modify'
},
{
nodeType: types.ruleNodeType.TRANSFORMATION.value,
type: 'New/Update'
},
{
nodeType: types.ruleNodeType.ACTION.value,
type: 'Telemetry'
},
{
nodeType: types.ruleNodeType.ACTION.value,
type: 'RPC call'
},
{
nodeType: types.ruleNodeType.ACTION.value,
type: 'Send email'
},
{
nodeType: types.ruleNodeType.ACTION.value,
type: 'Alarm'
}
]
);
return deferred.promise;
}
}

3
ui/src/app/app.js

@ -49,6 +49,7 @@ import 'material-ui';
import 'react-schema-form';
import react from 'ngreact';
import '@flowjs/ng-flow/dist/ng-flow-standalone.min';
import 'ngFlowchart/dist/ngFlowchart';
import thingsboardLocales from './locale/locale.constant';
import thingsboardLogin from './login';
@ -86,6 +87,7 @@ import 'mdPickers/dist/mdPickers.min.css';
import 'angular-hotkeys/build/hotkeys.min.css';
import 'angular-carousel/dist/angular-carousel.min.css';
import 'angular-material-expansion-panel/dist/md-expansion-panel.min.css';
import 'ngFlowchart/dist/flowchart.css';
import '../scss/main.scss';
import AppConfig from './app.config';
@ -113,6 +115,7 @@ angular.module('thingsboard', [
'ngclipboard',
react.name,
'flow',
'flowchart',
thingsboardLocales,
thingsboardLogin,
thingsboardDialogs,

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

@ -457,6 +457,44 @@ export default angular.module('thingsboard.types', [])
clientSide: false
}
},
ruleNodeType: {
FILTER: {
value: "FILTER",
name: "rulenode.type-filter",
nodeClass: "tb-filter-type",
icon: "filter_list"
},
ENRICHMENT: {
value: "ENRICHMENT",
name: "rulenode.type-enrichment",
nodeClass: "tb-enrichment-type",
icon: "playlist_add"
},
TRANSFORMATION: {
value: "TRANSFORMATION",
name: "rulenode.type-transformation",
nodeClass: "tb-transformation-type",
icon: "transform"
},
ACTION: {
value: "ACTION",
name: "rulenode.type-action",
nodeClass: "tb-action-type",
icon: "flash_on"
},
RULE_CHAIN: {
value: "RULE_CHAIN",
name: "rulenode.type-rule-chain",
nodeClass: "tb-rule-chain-type",
icon: "settings_ethernet"
},
INPUT: {
value: "INPUT",
nodeClass: "tb-input-type",
icon: "input",
special: true
}
},
valueType: {
string: {
value: "string",

6
ui/src/app/entity/entity-autocomplete.directive.js

@ -143,6 +143,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter
scope.noEntitiesMatchingText = 'plugin.no-plugins-matching';
scope.entityRequiredText = 'plugin.plugin-required';
break;
case types.entityType.rulechain:
scope.selectEntityText = 'rulechain.select-rulechain';
scope.entityText = 'rulechain.rulechain';
scope.noEntitiesMatchingText = 'rulechain.no-rulechains-matching';
scope.entityRequiredText = 'rulechain.rulechain-required';
break;
case types.entityType.tenant:
scope.selectEntityText = 'tenant.select-tenant';
scope.entityText = 'tenant.tenant';

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

@ -1169,6 +1169,26 @@ export default angular.module('thingsboard.locale', [])
"rulechain-required": "Rule chain is required",
"management": "Rules management"
},
"rulenode": {
"add": "Add rule node",
"name": "Name",
"name-required": "Name is required.",
"type": "Type",
"description": "Description",
"delete": "Delete rule node",
"rulenode-details": "Rule node details",
"link-details": "Rule node link details",
"add-link": "Add link",
"link-label": "Link label",
"link-label-required": "Link label is required.",
"custom-link-label": "Custom link label",
"custom-link-label-required": "Custom link label is required.",
"type-filter": "Filter",
"type-enrichment": "Enrichment",
"type-transformation": "Transformation",
"type-action": "Action",
"type-rule-chain": "Rule Chain"
},
"rule-plugin": {
"management": "Rules and plugins management"
},

48
ui/src/app/rulechain/add-link.tpl.html

@ -0,0 +1,48 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-dialog aria-label="{{ 'rulenode.add-link' | translate }}" tb-help="'rulechains'" help-container-id="help-container">
<form name="theForm" ng-submit="vm.add()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>rulenode.add-link</h2>
<span flex></span>
<div id="help-container"></div>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<tb-rule-node-link link="vm.link" labels="vm.labels" is-edit="true" the-form="theForm"></tb-rule-node-link>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary">
{{ 'action.add' | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

48
ui/src/app/rulechain/add-rulenode.tpl.html

@ -0,0 +1,48 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-dialog aria-label="{{ 'rulenode.add' | translate }}" tb-help="'rulechains'" help-container-id="help-container" style="min-width: 650px;">
<form name="theForm" ng-submit="vm.add()">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 translate>rulenode.add</h2>
<span flex></span>
<div id="help-container"></div>
<md-button class="md-icon-button" ng-click="vm.cancel()">
<ng-md-icon icon="close" aria-label="{{ 'dialog.close' | translate }}"></ng-md-icon>
</md-button>
</div>
</md-toolbar>
<md-progress-linear class="md-warn" md-mode="indeterminate" ng-disabled="!$root.loading" ng-show="$root.loading"></md-progress-linear>
<span style="min-height: 5px;" flex="" ng-show="!$root.loading"></span>
<md-dialog-content>
<div class="md-dialog-content">
<tb-rule-node rule-node="vm.ruleNode" rule-chain-id="vm.ruleChainId" is-edit="true" the-form="theForm"></tb-rule-node>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-disabled="$root.loading || theForm.$invalid || !theForm.$dirty" type="submit"
class="md-raised md-primary">
{{ 'action.add' | translate }}
</md-button>
<md-button ng-disabled="$root.loading" ng-click="vm.cancel()" style="margin-right:20px;">{{ 'action.cancel' |
translate }}
</md-button>
</md-dialog-actions>
</form>
</md-dialog>

10
ui/src/app/rulechain/index.js

@ -15,11 +15,19 @@
*/
import RuleChainRoutes from './rulechain.routes';
import RuleChainController from './rulechain.controller';
import RuleChainsController from './rulechains.controller';
import {RuleChainController, AddRuleNodeController, AddRuleNodeLinkController} from './rulechain.controller';
import RuleChainDirective from './rulechain.directive';
import RuleNodeDirective from './rulenode.directive';
import LinkDirective from './link.directive';
export default angular.module('thingsboard.ruleChain', [])
.config(RuleChainRoutes)
.controller('RuleChainsController', RuleChainsController)
.controller('RuleChainController', RuleChainController)
.controller('AddRuleNodeController', AddRuleNodeController)
.controller('AddRuleNodeLinkController', AddRuleNodeLinkController)
.directive('tbRuleChain', RuleChainDirective)
.directive('tbRuleNode', RuleNodeDirective)
.directive('tbRuleNodeLink', LinkDirective)
.name;

39
ui/src/app/rulechain/link-fieldset.tpl.html

@ -0,0 +1,39 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-content class="md-padding tb-link" layout="column">
<fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
<label translate>rulenode.link-label</label>
<md-select ng-model="selectedLabel" ng-change="selectedLabelChanged()">
<md-option ng-repeat="label in labels" ng-value="label">
{{label.name}}
</md-option>
</md-select>
<div ng-messages="theForm.linkLabel.$error">
<div translate ng-message="required">rulenode.link-label-required</div>
</div>
</md-input-container>
<md-input-container ng-if="selectedLabel.custom" class="md-block">
<label translate>rulenode.link-label</label>
<input required name="customLinkLabel" ng-model="link.label">
<div ng-messages="theForm.customLinkLabel.$error">
<div translate ng-message="required">rulenode.custom-link-label-required</div>
</div>
</md-input-container>
</fieldset>
</md-content>

71
ui/src/app/rulechain/link.directive.js

@ -0,0 +1,71 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import linkFieldsetTemplate from './link-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function LinkDirective($compile, $templateCache, $filter) {
var linker = function (scope, element) {
var template = $templateCache.get(linkFieldsetTemplate);
element.html(template);
scope.selectedLabel = null;
scope.$watch('link', function() {
scope.selectedLabel = null;
if (scope.link && scope.labels) {
if (scope.link.label) {
var result = $filter('filter')(scope.labels, {name: scope.link.label});
if (result && result.length) {
scope.selectedLabel = result[0];
} else {
result = $filter('filter')(scope.labels, {custom: true});
if (result && result.length && result[0].custom) {
scope.selectedLabel = result[0];
}
}
}
}
});
scope.selectedLabelChanged = function() {
if (scope.link && scope.selectedLabel) {
if (!scope.selectedLabel.custom) {
scope.link.label = scope.selectedLabel.name;
} else {
scope.link.label = "";
}
}
};
$compile(element.contents())(scope);
}
return {
restrict: "E",
link: linker,
scope: {
link: '=',
labels: '=',
isEdit: '=',
isReadOnly: '=',
theForm: '='
}
};
}

2
ui/src/app/rulechain/rulechain-fieldset.tpl.html

@ -32,7 +32,7 @@
</md-button>
</div>
<md-content class="md-padding tb-rulechain" layout="column">
<md-content class="md-padding tb-rulechain-fieldset" layout="column">
<fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
<label translate>rulechain.name</label>

638
ui/src/app/rulechain/rulechain.controller.js

@ -13,160 +13,582 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import './rulechain.scss';
/* eslint-disable import/no-unresolved, import/default */
import addRuleChainTemplate from './add-rulechain.tpl.html';
import ruleChainCard from './rulechain-card.tpl.html';
import addRuleNodeTemplate from './add-rulenode.tpl.html';
import addRuleNodeLinkTemplate from './add-link.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
const deleteKeyCode = 46;
const ctrlKeyCode = 17;
const aKeyCode = 65;
const escKeyCode = 27;
/*@ngInject*/
export default function RuleChainController(ruleChainService, userService, importExport, $state, $stateParams, $filter, $translate, types) {
var ruleChainActionsList = [
{
onAction: function ($event, item) {
exportRuleChain($event, item);
},
name: function() { $translate.instant('action.export') },
details: function() { return $translate.instant('rulechain.export') },
icon: "file_download"
},
{
onAction: function ($event, item) {
vm.grid.deleteItem($event, item);
},
name: function() { return $translate.instant('action.delete') },
details: function() { return $translate.instant('rulechain.delete') },
icon: "delete",
isEnabled: isRuleChainEditable
}
];
var ruleChainAddItemActionsList = [
{
onAction: function ($event) {
vm.grid.addItem($event);
},
name: function() { return $translate.instant('action.create') },
details: function() { return $translate.instant('rulechain.create-new-rulechain') },
icon: "insert_drive_file"
},
{
onAction: function ($event) {
importExport.importRuleChain($event).then(
function() {
vm.grid.refreshList();
}
);
},
name: function() { return $translate.instant('action.import') },
details: function() { return $translate.instant('rulechain.import') },
icon: "file_upload"
}
];
export function RuleChainController($stateParams, $scope, $q, $mdUtil, $mdExpansionPanel, $document, $mdDialog, $filter, types, ruleChainService, Modelfactory, flowchartConstants, ruleChain, ruleChainMetaData) {
var vm = this;
vm.$mdExpansionPanel = $mdExpansionPanel;
vm.types = types;
vm.ruleChainGridConfig = {
vm.editingRuleNode = null;
vm.isEditingRuleNode = false;
vm.editingRuleNodeLink = null;
vm.isEditingRuleNodeLink = false;
refreshParamsFunc: null,
vm.ruleChain = ruleChain;
vm.ruleChainMetaData = ruleChainMetaData;
deleteItemTitleFunc: deleteRuleChainTitle,
deleteItemContentFunc: deleteRuleChainText,
deleteItemsTitleFunc: deleteRuleChainsTitle,
deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
deleteItemsContentFunc: deleteRuleChainsText,
vm.canvasControl = {};
fetchItemsFunc: fetchRuleChains,
saveItemFunc: saveRuleChain,
deleteItemFunc: deleteRuleChain,
vm.ruleChainModel = {
nodes: [],
edges: []
};
vm.ruleNodeTypesModel = {};
for (var type in types.ruleNodeType) {
if (!types.ruleNodeType[type].special) {
vm.ruleNodeTypesModel[type] = {
model: {
nodes: [],
edges: []
},
selectedObjects: []
};
}
}
getItemTitleFunc: getRuleChainTitle,
itemCardTemplateUrl: ruleChainCard,
parentCtl: vm,
vm.selectedObjects = [];
actionsList: ruleChainActionsList,
addItemActions: ruleChainAddItemActionsList,
vm.modelservice = Modelfactory(vm.ruleChainModel, vm.selectedObjects);
onGridInited: gridInited,
vm.ctrlDown = false;
addItemTemplateUrl: addRuleChainTemplate,
vm.saveRuleChain = saveRuleChain;
vm.revertRuleChain = revertRuleChain;
addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
isSelectionEnabled: isRuleChainEditable,
isDetailsReadOnly: function(ruleChain) {
return !isRuleChainEditable(ruleChain);
vm.keyDown = function (evt) {
if (evt.keyCode === ctrlKeyCode) {
vm.ctrlDown = true;
evt.stopPropagation();
evt.preventDefault();
}
};
if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
vm.ruleChainGridConfig.items = $stateParams.items;
}
vm.keyUp = function (evt) {
if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
}
if (evt.keyCode === deleteKeyCode) {
vm.modelservice.deleteSelected();
}
vm.isRuleChainEditable = isRuleChainEditable;
if (evt.keyCode == aKeyCode && vm.ctrlDown) {
vm.modelservice.selectAll();
}
vm.exportRuleChain = exportRuleChain;
if (evt.keyCode == escKeyCode) {
vm.modelservice.deselectAll();
}
function deleteRuleChainTitle(ruleChain) {
return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
}
if (evt.keyCode === ctrlKeyCode) {
vm.ctrlDown = false;
evt.stopPropagation();
evt.preventDefault();
}
};
function deleteRuleChainText() {
return $translate.instant('rulechain.delete-rulechain-text');
}
vm.onEditRuleNodeClosed = function() {
vm.editingRuleNode = null;
};
vm.onEditRuleNodeLinkClosed = function() {
vm.editingRuleNodeLink = null;
};
vm.saveRuleNode = function(theForm) {
theForm.$setPristine();
vm.isEditingRuleNode = false;
vm.ruleChainModel.nodes[vm.editingRuleNodeIndex] = vm.editingRuleNode;
vm.editingRuleNode = angular.copy(vm.editingRuleNode);
};
vm.saveRuleNodeLink = function(theForm) {
theForm.$setPristine();
vm.isEditingRuleNodeLink = false;
vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex] = vm.editingRuleNodeLink;
vm.editingRuleNodeLink = angular.copy(vm.editingRuleNodeLink);
};
vm.onRevertRuleNodeEdit = function(theForm) {
theForm.$setPristine();
var node = vm.ruleChainModel.nodes[vm.editingRuleNodeIndex];
vm.editingRuleNode = angular.copy(node);
};
vm.onRevertRuleNodeLinkEdit = function(theForm) {
theForm.$setPristine();
var edge = vm.ruleChainModel.edges[vm.editingRuleNodeLinkIndex];
vm.editingRuleNodeLink = angular.copy(edge);
};
vm.editCallbacks = {
edgeDoubleClick: function (event, edge) {
var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
if (sourceNode.nodeType != types.ruleNodeType.INPUT.value) {
ruleChainService.getRuleNodeSupportedLinks(sourceNode.type).then(
(labels) => {
vm.isEditingRuleNode = false;
vm.editingRuleNode = null;
vm.editingRuleNodeLinkLabels = labels;
vm.isEditingRuleNodeLink = true;
vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
vm.editingRuleNodeLink = angular.copy(edge);
}
);
}
},
nodeCallbacks: {
'doubleClick': function (event, node) {
if (node.nodeType != types.ruleNodeType.INPUT.value) {
vm.isEditingRuleNodeLink = false;
vm.editingRuleNodeLink = null;
vm.isEditingRuleNode = true;
vm.editingRuleNodeIndex = vm.ruleChainModel.nodes.indexOf(node);
vm.editingRuleNode = angular.copy(node);
}
}
},
isValidEdge: function (source, destination) {
return source.type === flowchartConstants.rightConnectorType && destination.type === flowchartConstants.leftConnectorType;
},
createEdge: function (event, edge) {
var deferred = $q.defer();
var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
if (sourceNode.nodeType == types.ruleNodeType.INPUT.value) {
var destNode = vm.modelservice.nodes.getNodeByConnectorId(edge.destination);
if (destNode.nodeType == types.ruleNodeType.RULE_CHAIN.value) {
deferred.reject();
} else {
var res = $filter('filter')(vm.ruleChainModel.edges, {source: vm.inputConnectorId});
if (res && res.length) {
vm.modelservice.edges.delete(res[0]);
}
deferred.resolve(edge);
}
} else {
ruleChainService.getRuleNodeSupportedLinks(sourceNode.type).then(
(labels) => {
addRuleNodeLink(event, edge, labels).then(
(link) => {
deferred.resolve(link);
},
() => {
deferred.reject();
}
);
},
() => {
deferred.reject();
}
);
}
return deferred.promise;
},
dropNode: function (event, node) {
addRuleNode(event, node);
}
};
function deleteRuleChainsTitle(selectedCount) {
return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
loadRuleChainLibrary();
function loadRuleChainLibrary() {
ruleChainService.getRuleNodeTypes().then(
(ruleNodeTypes) => {
for (var i=0;i<ruleNodeTypes.length;i++) {
var ruleNodeType = ruleNodeTypes[i];
var nodeType = ruleNodeType.nodeType;
var model = vm.ruleNodeTypesModel[nodeType].model;
var node = {
id: model.nodes.length,
nodeType: nodeType,
type: ruleNodeType.type,
name: '',
nodeClass: vm.types.ruleNodeType[nodeType].nodeClass,
icon: vm.types.ruleNodeType[nodeType].icon,
x: 30,
y: 10+50*model.nodes.length,
connectors: []
};
if (nodeType == types.ruleNodeType.RULE_CHAIN.value) {
node.connectors.push(
{
type: flowchartConstants.leftConnectorType,
id: model.nodes.length
}
);
} else {
node.connectors.push(
{
type: flowchartConstants.leftConnectorType,
id: model.nodes.length*2
}
);
node.connectors.push(
{
type: flowchartConstants.rightConnectorType,
id: model.nodes.length*2+1
}
);
}
model.nodes.push(node);
}
prepareRuleChain();
}
);
}
function deleteRuleChainsActionTitle(selectedCount) {
return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
function prepareRuleChain() {
if (vm.ruleChainWatch) {
vm.ruleChainWatch();
vm.ruleChainWatch = null;
}
vm.nextNodeID = 1;
vm.nextConnectorID = 1;
vm.selectedObjects.length = 0;
vm.ruleChainModel.nodes.length = 0;
vm.ruleChainModel.edges.length = 0;
vm.inputConnectorId = vm.nextConnectorID++;
vm.ruleChainModel.nodes.push(
{
id: vm.nextNodeID++,
type: "Input",
name: "",
nodeType: types.ruleNodeType.INPUT.value,
nodeClass: types.ruleNodeType.INPUT.nodeClass,
icon: types.ruleNodeType.INPUT.icon,
readonly: true,
x: 50,
y: 150,
connectors: [
{
type: flowchartConstants.rightConnectorType,
id: vm.inputConnectorId
},
]
}
);
ruleChainService.resolveTargetRuleChains(vm.ruleChainMetaData.ruleChainConnections)
.then((ruleChainsMap) => {
createRuleChainModel(ruleChainsMap);
}
);
}
function deleteRuleChainsText() {
return $translate.instant('rulechain.delete-rulechains-text');
function createRuleChainModel(ruleChainsMap) {
var nodes = [];
for (var i=0;i<vm.ruleChainMetaData.nodes.length;i++) {
var ruleNode = vm.ruleChainMetaData.nodes[i];
var nodeType = ruleChainService.getRuleNodeComponentType(ruleNode.type);
if (nodeType) {
var node = {
id: vm.nextNodeID++,
ruleNodeId: ruleNode.id,
additionalInfo: ruleNode.additionalInfo,
configuration: ruleNode.configuration,
x: ruleNode.additionalInfo.layoutX,
y: ruleNode.additionalInfo.layoutY,
type: ruleNode.type,
name: ruleNode.name,
nodeType: nodeType,
nodeClass: vm.types.ruleNodeType[nodeType].nodeClass,
icon: vm.types.ruleNodeType[nodeType].icon,
connectors: [
{
type: flowchartConstants.leftConnectorType,
id: vm.nextConnectorID++
},
{
type: flowchartConstants.rightConnectorType,
id: vm.nextConnectorID++
}
]
};
nodes.push(node);
vm.ruleChainModel.nodes.push(node);
}
}
if (vm.ruleChainMetaData.firstNodeIndex > -1) {
var destNode = nodes[vm.ruleChainMetaData.firstNodeIndex];
if (destNode) {
var connectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
if (connectors && connectors.length) {
var edge = {
source: vm.inputConnectorId,
destination: connectors[0].id
};
vm.ruleChainModel.edges.push(edge);
}
}
}
if (vm.ruleChainMetaData.connections) {
for (i = 0; i < vm.ruleChainMetaData.connections.length; i++) {
var connection = vm.ruleChainMetaData.connections[0];
var sourceNode = nodes[connection.fromIndex];
destNode = nodes[connection.toIndex];
if (sourceNode && destNode) {
var sourceConnectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
var destConnectors = vm.modelservice.nodes.getConnectorsByType(destNode, flowchartConstants.leftConnectorType);
if (sourceConnectors && sourceConnectors.length && destConnectors && destConnectors.length) {
edge = {
source: sourceConnectors[0].id,
destination: destConnectors[0].id,
label: connection.type
};
vm.ruleChainModel.edges.push(edge);
}
}
}
}
if (vm.ruleChainMetaData.ruleChainConnections) {
var ruleChainNodesMap = {};
for (i = 0; i < vm.ruleChainMetaData.ruleChainConnections.length; i++) {
var ruleChainConnection = vm.ruleChainMetaData.ruleChainConnections[i];
var ruleChain = ruleChainsMap[ruleChainConnection.targetRuleChainId.id];
if (ruleChainConnection.additionalInfo && ruleChainConnection.additionalInfo.ruleChainNodeId) {
var ruleChainNode = ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId];
if (!ruleChainNode) {
ruleChainNode = {
id: vm.nextNodeID++,
additionalInfo: ruleChainConnection.additionalInfo,
targetRuleChainId: ruleChainConnection.targetRuleChainId.id,
x: ruleChainConnection.additionalInfo.layoutX,
y: ruleChainConnection.additionalInfo.layoutY,
type: 'Rule chain',
name: ruleChain.name,
nodeType: vm.types.ruleNodeType.RULE_CHAIN.value,
nodeClass: vm.types.ruleNodeType.RULE_CHAIN.nodeClass,
icon: vm.types.ruleNodeType.RULE_CHAIN.icon,
connectors: [
{
type: flowchartConstants.leftConnectorType,
id: vm.nextConnectorID++
}
]
};
ruleChainNodesMap[ruleChainConnection.additionalInfo.ruleChainNodeId] = ruleChainNode;
vm.ruleChainModel.nodes.push(ruleChainNode);
}
sourceNode = nodes[ruleChainConnection.fromIndex];
if (sourceNode) {
connectors = vm.modelservice.nodes.getConnectorsByType(sourceNode, flowchartConstants.rightConnectorType);
if (connectors && connectors.length) {
var ruleChainEdge = {
source: connectors[0].id,
destination: ruleChainNode.connectors[0].id,
label: ruleChainConnection.type
};
vm.ruleChainModel.edges.push(ruleChainEdge);
}
}
}
}
}
vm.canvasControl.adjustCanvasSize();
vm.isDirty = false;
$mdUtil.nextTick(() => {
vm.ruleChainWatch = $scope.$watch('vm.ruleChainModel',
function (newVal, oldVal) {
if (!vm.isDirty && !angular.equals(newVal, oldVal)) {
vm.isDirty = true;
}
}, true
);
});
}
function gridInited(grid) {
vm.grid = grid;
function saveRuleChain() {
var ruleChainMetaData = {
ruleChainId: vm.ruleChain.id,
nodes: [],
connections: [],
ruleChainConnections: []
};
var nodes = [];
for (var i=0;i<vm.ruleChainModel.nodes.length;i++) {
var node = vm.ruleChainModel.nodes[i];
if (node.nodeType != types.ruleNodeType.INPUT.value && node.nodeType != types.ruleNodeType.RULE_CHAIN.value) {
var ruleNode = {};
if (node.ruleNodeId) {
ruleNode.id = node.ruleNodeId;
}
ruleNode.type = node.type;
ruleNode.name = node.name;
ruleNode.configuration = node.configuration;
ruleNode.additionalInfo = node.additionalInfo;
if (!ruleNode.additionalInfo) {
ruleNode.additionalInfo = {};
}
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.nodeType != types.ruleNodeType.INPUT.value) {
var fromIndex = nodes.indexOf(sourceNode);
if (destNode.nodeType == 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);
}
}
}
ruleChainService.saveRuleChainMetaData(ruleChainMetaData).then(
(ruleChainMetaData) => {
vm.ruleChainMetaData = ruleChainMetaData;
prepareRuleChain();
}
);
}
function fetchRuleChains(pageLink) {
return ruleChainService.getRuleChains(pageLink);
function revertRuleChain() {
prepareRuleChain();
}
function saveRuleChain(ruleChain) {
return ruleChainService.saveRuleChain(ruleChain);
function addRuleNode($event, ruleNode) {
$mdDialog.show({
controller: 'AddRuleNodeController',
controllerAs: 'vm',
templateUrl: addRuleNodeTemplate,
parent: angular.element($document[0].body),
locals: {ruleNode: ruleNode, ruleChainId: vm.ruleChain.id.id},
fullscreen: true,
targetEvent: $event
}).then(function (ruleNode) {
ruleNode.id = vm.nextNodeID++;
ruleNode.connectors = [];
ruleNode.connectors.push(
{
id: vm.nextConnectorID++,
type: flowchartConstants.leftConnectorType
}
);
if (ruleNode.nodeType != types.ruleNodeType.RULE_CHAIN.value) {
ruleNode.connectors.push(
{
id: vm.nextConnectorID++,
type: flowchartConstants.rightConnectorType
}
);
}
vm.ruleChainModel.nodes.push(ruleNode);
}, function () {
});
}
function deleteRuleChain(ruleChainId) {
return ruleChainService.deleteRuleChain(ruleChainId);
function addRuleNodeLink($event, link, labels) {
return $mdDialog.show({
controller: 'AddRuleNodeLinkController',
controllerAs: 'vm',
templateUrl: addRuleNodeLinkTemplate,
parent: angular.element($document[0].body),
locals: {link: link, labels: labels},
fullscreen: true,
targetEvent: $event
});
}
function getRuleChainTitle(ruleChain) {
return ruleChain ? ruleChain.name : '';
}
/*@ngInject*/
export function AddRuleNodeController($scope, $mdDialog, ruleNode, ruleChainId, helpLinks) {
var vm = this;
vm.helpLinks = helpLinks;
vm.ruleNode = ruleNode;
vm.ruleChainId = ruleChainId;
vm.add = add;
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
function isRuleChainEditable(ruleChain) {
if (userService.getAuthority() === 'TENANT_ADMIN') {
return ruleChain && ruleChain.tenantId.id != types.id.nullUid;
} else {
return userService.getAuthority() === 'SYS_ADMIN';
}
function add() {
$scope.theForm.$setPristine();
$mdDialog.hide(vm.ruleNode);
}
}
/*@ngInject*/
export function AddRuleNodeLinkController($scope, $mdDialog, link, labels, helpLinks) {
function exportRuleChain($event, ruleChain) {
$event.stopPropagation();
importExport.exportRuleChain(ruleChain.id.id);
var vm = this;
vm.helpLinks = helpLinks;
vm.link = link;
vm.labels = labels;
vm.add = add;
vm.cancel = cancel;
function cancel() {
$mdDialog.cancel();
}
function add() {
$scope.theForm.$setPristine();
$mdDialog.hide(vm.link);
}
}

41
ui/src/app/rulechain/rulechain.routes.js

@ -15,12 +15,16 @@
*/
/* eslint-disable import/no-unresolved, import/default */
import ruleNodeTemplate from './rulenode.tpl.html';
import ruleChainsTemplate from './rulechains.tpl.html';
import ruleChainTemplate from './rulechain.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RuleChainRoutes($stateProvider) {
export default function RuleChainRoutes($stateProvider, NodeTemplatePathProvider) {
NodeTemplatePathProvider.setTemplatePath(ruleNodeTemplate);
$stateProvider
.state('home.ruleChains', {
@ -32,7 +36,7 @@ export default function RuleChainRoutes($stateProvider) {
"content@home": {
templateUrl: ruleChainsTemplate,
controllerAs: 'vm',
controller: 'RuleChainController'
controller: 'RuleChainsController'
}
},
data: {
@ -42,5 +46,36 @@ export default function RuleChainRoutes($stateProvider) {
ncyBreadcrumb: {
label: '{"icon": "settings_ethernet", "label": "rulechain.rulechains"}'
}
});
}).state('home.ruleChains.ruleChain', {
url: '/:ruleChainId',
reloadOnSearch: false,
module: 'private',
auth: ['SYS_ADMIN', 'TENANT_ADMIN'],
views: {
"content@home": {
templateUrl: ruleChainTemplate,
controller: 'RuleChainController',
controllerAs: 'vm'
}
},
resolve: {
ruleChain:
/*@ngInject*/
function($stateParams, ruleChainService) {
return ruleChainService.getRuleChain($stateParams.ruleChainId);
},
ruleChainMetaData:
/*@ngInject*/
function($stateParams, ruleChainService) {
return ruleChainService.getRuleChainMetaData($stateParams.ruleChainId);
}
},
data: {
searchEnabled: false,
pageTitle: 'rulechain.rulechain'
},
ncyBreadcrumb: {
label: '{"icon": "settings_ethernet", "label": "{{ vm.ruleChain.name }}", "translate": "false"}'
}
});
}

262
ui/src/app/rulechain/rulechain.scss

@ -0,0 +1,262 @@
/**
* Copyright © 2016-2018 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-rulechain {
.tb-fullscreen-button-style {
z-index: 1;
}
.tb-rulechain-library {
width: 250px;
min-width: 250px;
overflow-y: auto;
overflow-x: hidden;
.tb-rulechain-library-panel-group {
.tb-panel-title {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
min-width: 180px;
}
.fc-canvas {
background: none;
}
md-icon.md-expansion-panel-icon {
margin-right: 0px;
}
md-expansion-panel-collapsed, .md-expansion-panel-header-container {
background: #e6e6e6;
border-color: #909090;
position: static;
}
md-expansion-panel {
&.md-open {
margin-top: 0;
margin-bottom: 0;
}
}
md-expansion-panel-content {
padding: 0px;
}
}
}
.tb-rulechain-graph {
overflow: auto;
}
}
.fc-canvas {
min-width: 100%;
min-height: 100%;
outline: none;
}
.tb-rule-node {
display: flex;
flex-direction: row;
min-width: 150px;
max-width: 150px;
min-height: 28px;
max-height: 28px;
padding: 5px 10px;
border-radius: 5px;
background-color: #F15B26;
color: #333;
border: solid 1px #777;
font-size: 12px;
&.tb-input-type {
background-color: #a3eaa9;
user-select: none;
}
&.tb-filter-type {
background-color: #f1e861;
}
&.tb-enrichment-type {
background-color: #cdf14e;
}
&.tb-transformation-type {
background-color: #79cef1;
}
&.tb-action-type {
background-color: #f1928f;
}
&.tb-rule-chain-type {
background-color: #d6c4f1;
}
md-icon {
font-size: 20px;
width: 20px;
height: 20px;
min-height: 20px;
min-width: 20px;
padding-right: 4px;
}
.tb-node-type {
}
.tb-node-title {
font-weight: 600;
}
.tb-node-type, .tb-node-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.fc-node {
z-index: 1;
outline: none;
&.fc-hover, &.fc-selected {
-webkit-filter: brightness(70%);
filter: brightness(70%);
}
&.fc-dragging {
z-index: 10;
}
p {
padding: 0 15px;
text-align: center;
}
}
.fc-leftConnectors, .fc-rightConnectors {
position: absolute;
top: 0;
height: 100%;
display: flex;
flex-direction: column;
z-index: 0;
.fc-magnet {
align-items: center;
}
}
.fc-leftConnectors {
left: -20px;
}
.fc-rightConnectors {
right: -20px;
}
.fc-magnet {
display: flex;
flex-grow: 1;
height: 60px;
justify-content: center;
}
.fc-connector {
width: 14px;
height: 14px;
border: 1px solid #333;
margin: 10px;
border-radius: 5px;
background-color: #ccc;
}
.fc-connector.fc-hover {
background-color: #000;
}
.fc-edge {
outline: none;
stroke: gray;
stroke-width: 4;
fill: transparent;
&.fc-selected {
stroke: red;
stroke-width: 4;
fill: transparent;
}
&.fc-active {
animation: dash 3s linear infinite;
stroke-dasharray: 20;
}
&.fc-hover {
stroke: gray;
stroke-width: 6;
fill: transparent;
}
&.fc-dragging {
pointer-events: none;
}
}
.edge-endpoint {
fill: gray;
}
.fc-nodedelete {
display: none;
}
.fc-selected .fc-nodedelete {
outline: none;
display: block;
position: absolute;
right: -13px;
top: -16px;
border: solid 2px white;
border-radius: 50%;
font-weight: 600;
font-size: 18px;
line-height: 18px;
height: 20px;
padding-top: 2px;
width: 22px;
background: #494949;
color: #fff;
text-align: center;
vertical-align: bottom;
cursor: pointer;
}
.fc-edge-label {
position: absolute;
user-select: none;
pointer-events: none;
opacity: 0.8;
}
.fc-edge-label-text {
position: absolute;
left: 50%;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
white-space: nowrap;
text-align: center;
font-size: 14px;
font-weight: 600;
top: 5px;
span {
border: solid 2px #003a79;
border-radius: 10px;
color: #003a79;
background-color: #fff;
padding: 3px 5px;
}
}
@keyframes dash {
from {
stroke-dashoffset: 500;
}
}

130
ui/src/app/rulechain/rulechain.tpl.html

@ -0,0 +1,130 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-content flex tb-expand-fullscreen
expand-tooltip-direction="bottom" layout="column" class="tb-rulechain">
<section class="tb-rulechain-container" flex layout="column">
<div class="tb-rulechain-layout" flex layout="row">
<div class="tb-rulechain-library">
<md-expansion-panel-group class="tb-rulechain-library-panel-group" md-component-id="libraryPanelGroup" auto-expand="true" multiple>
<md-expansion-panel md-component-id="{{typeId}}" id="{{typeId}}" ng-repeat="(typeId, typeModel) in vm.ruleNodeTypesModel">
<md-expansion-panel-collapsed>
<div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
<md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-collapsed>
<md-expansion-panel-expanded>
<md-expansion-panel-header ng-click="vm.$mdExpansionPanel(typeId).collapse()">
<div class="tb-panel-title" translate>{{vm.types.ruleNodeType[typeId].name}}</div>
<md-expansion-panel-icon></md-expansion-panel-icon>
</md-expansion-panel-header>
<md-expansion-panel-content>
<fc-canvas id="tb-rulechain-{{typeId}}"
model="vm.ruleNodeTypesModel[typeId].model" selected-objects="vm.ruleNodeTypesModel[typeId].selectedObjects"
automatic-resize="false"
node-width="170"
node-height="50"
drop-target-id="'tb-rulchain-canvas'"></fc-canvas>
</md-expansion-panel-content>
</md-expansion-panel-expanded>
</md-expansion-panel>
</md-expansion-panel-group>
</div>
<div flex class="tb-rulechain-graph">
<fc-canvas id="tb-rulchain-canvas"
ng-keydown="vm.keyDown($event)"
ng-keyup="vm.keyUp($event)"
model="vm.ruleChainModel"
selected-objects="vm.selectedObjects"
edge-style="curved"
node-width="170"
node-height="50"
automatic-resize="true"
control="vm.canvasControl"
callbacks="vm.editCallbacks">
</fc-canvas>
</div>
</div>
<tb-details-sidenav class="tb-rulenode-details-sidenav"
header-title="{{vm.editingRuleNode.name}}"
header-subtitle="{{'rulenode.rulenode-details' | translate}}"
is-read-only="false"
is-open="vm.isEditingRuleNode"
is-always-edit="true"
on-close-details="vm.onEditRuleNodeClosed()"
on-toggle-details-edit-mode="vm.onRevertRuleNodeEdit(vm.ruleNodeForm)"
on-apply-details="vm.saveRuleNode(vm.ruleNodeForm)"
the-form="vm.ruleNodeForm">
<details-buttons tb-help="vm.helpLinkIdForRuleNodeType()" help-container-id="help-container">
<div id="help-container"></div>
</details-buttons>
<form name="vm.ruleNodeForm" ng-if="vm.isEditingRuleNode">
<tb-rule-node
rule-node="vm.editingRuleNode"
rule-chain-id="vm.ruleChain.id.id"
is-edit="true"
is-read-only="false"
on-delete-rule-node="vm.deleteRuleNode(event, vm.editingRuleNode)"
the-form="vm.ruleNodeForm">
</tb-rule-node>
</form>
</tb-details-sidenav>
<tb-details-sidenav class="tb-rulenode-link-details-sidenav"
header-title="{{vm.editingRuleNodeLink.label}}"
header-subtitle="{{'rulenode.link-details' | translate}}"
is-read-only="false"
is-open="vm.isEditingRuleNodeLink"
is-always-edit="true"
on-close-details="vm.onEditRuleNodeLinkClosed()"
on-toggle-details-edit-mode="vm.onRevertRuleNodeLinkEdit(vm.ruleNodeLinkForm)"
on-apply-details="vm.saveRuleNodeLink(vm.ruleNodeLinkForm)"
the-form="vm.ruleNodeLinkForm">
<details-buttons tb-help="vm.helpLinkIdForRuleNodeLink()" help-container-id="link-help-container">
<div id="link-help-container"></div>
</details-buttons>
<form name="vm.ruleNodeLinkForm" ng-if="vm.isEditingRuleNodeLink">
<tb-rule-node-link
link="vm.editingRuleNodeLink"
labels="vm.editingRuleNodeLinkLabels"
is-edit="true"
is-read-only="false"
the-form="vm.ruleNodeLinkForm">
</tb-rule-node-link>
</form>
</tb-details-sidenav>
</section>
<section layout="row" layout-wrap class="tb-footer-buttons md-fab" layout-align="start end">
<md-button ng-disabled="$root.loading || !vm.isDirty"
class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.apply' | translate }}"
ng-click="vm.saveRuleChain()">
<md-tooltip md-direction="top">
{{ 'action.apply-changes' | translate }}
</md-tooltip>
<ng-md-icon icon="done"></ng-md-icon>
</md-button>
<md-button ng-disabled="$root.loading || !vm.isDirty"
class="tb-btn-footer md-accent md-hue-2 md-fab"
aria-label="{{ 'action.decline-changes' | translate }}"
ng-click="vm.revertRuleChain()">
<md-tooltip md-direction="top">
{{ 'action.decline-changes' | translate }}
</md-tooltip>
<ng-md-icon icon="close"></ng-md-icon>
</md-button>
</section>
</md-content>

188
ui/src/app/rulechain/rulechains.controller.js

@ -0,0 +1,188 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import addRuleChainTemplate from './add-rulechain.tpl.html';
import ruleChainCard from './rulechain-card.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RuleChainsController(ruleChainService, userService, importExport, $state, $stateParams, $filter, $translate, types) {
var ruleChainActionsList = [
{
onAction: function ($event, item) {
vm.grid.openItem($event, item);
},
name: function() { return $translate.instant('rulechain.details') },
details: function() { return $translate.instant('rulechain.rulechain-details') },
icon: "edit"
},
{
onAction: function ($event, item) {
exportRuleChain($event, item);
},
name: function() { $translate.instant('action.export') },
details: function() { return $translate.instant('rulechain.export') },
icon: "file_download"
},
{
onAction: function ($event, item) {
vm.grid.deleteItem($event, item);
},
name: function() { return $translate.instant('action.delete') },
details: function() { return $translate.instant('rulechain.delete') },
icon: "delete",
isEnabled: isRuleChainEditable
}
];
var ruleChainAddItemActionsList = [
{
onAction: function ($event) {
vm.grid.addItem($event);
},
name: function() { return $translate.instant('action.create') },
details: function() { return $translate.instant('rulechain.create-new-rulechain') },
icon: "insert_drive_file"
},
{
onAction: function ($event) {
importExport.importRuleChain($event).then(
function() {
vm.grid.refreshList();
}
);
},
name: function() { return $translate.instant('action.import') },
details: function() { return $translate.instant('rulechain.import') },
icon: "file_upload"
}
];
var vm = this;
vm.types = types;
vm.ruleChainGridConfig = {
refreshParamsFunc: null,
deleteItemTitleFunc: deleteRuleChainTitle,
deleteItemContentFunc: deleteRuleChainText,
deleteItemsTitleFunc: deleteRuleChainsTitle,
deleteItemsActionTitleFunc: deleteRuleChainsActionTitle,
deleteItemsContentFunc: deleteRuleChainsText,
fetchItemsFunc: fetchRuleChains,
saveItemFunc: saveRuleChain,
clickItemFunc: openRuleChain,
deleteItemFunc: deleteRuleChain,
getItemTitleFunc: getRuleChainTitle,
itemCardTemplateUrl: ruleChainCard,
parentCtl: vm,
actionsList: ruleChainActionsList,
addItemActions: ruleChainAddItemActionsList,
onGridInited: gridInited,
addItemTemplateUrl: addRuleChainTemplate,
addItemText: function() { return $translate.instant('rulechain.add-rulechain-text') },
noItemsText: function() { return $translate.instant('rulechain.no-rulechains-text') },
itemDetailsText: function() { return $translate.instant('rulechain.rulechain-details') },
isSelectionEnabled: isRuleChainEditable,
isDetailsReadOnly: function(ruleChain) {
return !isRuleChainEditable(ruleChain);
}
};
if (angular.isDefined($stateParams.items) && $stateParams.items !== null) {
vm.ruleChainGridConfig.items = $stateParams.items;
}
if (angular.isDefined($stateParams.topIndex) && $stateParams.topIndex > 0) {
vm.ruleChainGridConfig.topIndex = $stateParams.topIndex;
}
vm.isRuleChainEditable = isRuleChainEditable;
vm.exportRuleChain = exportRuleChain;
function deleteRuleChainTitle(ruleChain) {
return $translate.instant('rulechain.delete-rulechain-title', {ruleChainName: ruleChain.name});
}
function deleteRuleChainText() {
return $translate.instant('rulechain.delete-rulechain-text');
}
function deleteRuleChainsTitle(selectedCount) {
return $translate.instant('rulechain.delete-rulechains-title', {count: selectedCount}, 'messageformat');
}
function deleteRuleChainsActionTitle(selectedCount) {
return $translate.instant('rulechain.delete-rulechains-action-title', {count: selectedCount}, 'messageformat');
}
function deleteRuleChainsText() {
return $translate.instant('rulechain.delete-rulechains-text');
}
function gridInited(grid) {
vm.grid = grid;
}
function fetchRuleChains(pageLink) {
return ruleChainService.getRuleChains(pageLink);
}
function saveRuleChain(ruleChain) {
return ruleChainService.saveRuleChain(ruleChain);
}
function openRuleChain($event, ruleChain) {
if ($event) {
$event.stopPropagation();
}
$state.go('home.ruleChains.ruleChain', {ruleChainId: ruleChain.id.id});
}
function deleteRuleChain(ruleChainId) {
return ruleChainService.deleteRuleChain(ruleChainId);
}
function getRuleChainTitle(ruleChain) {
return ruleChain ? ruleChain.name : '';
}
function isRuleChainEditable(ruleChain) {
if (userService.getAuthority() === 'TENANT_ADMIN') {
return ruleChain && ruleChain.tenantId.id != types.id.nullUid;
} else {
return userService.getAuthority() === 'SYS_ADMIN';
}
}
function exportRuleChain($event, ruleChain) {
$event.stopPropagation();
importExport.exportRuleChain(ruleChain.id.id);
}
}

55
ui/src/app/rulechain/rulenode-fieldset.tpl.html

@ -0,0 +1,55 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-button ng-click="onDeleteRuleNode({event: $event})"
ng-show="!isEdit && !isReadOnly"
class="md-raised md-primary">{{ 'rulenode.delete' | translate }}</md-button>
<md-content class="md-padding tb-rulenode" layout="column">
<fieldset ng-disabled="$root.loading || !isEdit || isReadOnly">
<md-input-container class="md-block">
<label translate>rulenode.type</label>
<input readonly name="type" ng-model="ruleNode.type">
</md-input-container>
<section ng-if="ruleNode.nodeType != types.ruleNodeType.RULE_CHAIN.value">
<md-input-container class="md-block">
<label translate>rulenode.name</label>
<input required name="name" ng-model="ruleNode.name">
<div ng-messages="theForm.name.$error">
<div translate ng-message="required">rulenode.name-required</div>
</div>
</md-input-container>
<md-input-container class="md-block">
<label translate>rulenode.description</label>
<textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
</md-input-container>
</section>
<section ng-if="ruleNode.nodeType == types.ruleNodeType.RULE_CHAIN.value">
<tb-entity-autocomplete the-form="theForm"
ng-disabled="$root.loading || !isEdit || isReadOnly"
tb-required="true"
exclude-entity-ids="[ruleChainId]"
entity-type="types.entityType.rulechain"
ng-model="params.targetRuleChainId">
</tb-entity-autocomplete>
<md-input-container class="md-block">
<label translate>rulenode.description</label>
<textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
</md-input-container>
</section>
</fieldset>
</md-content>

79
ui/src/app/rulechain/rulenode.directive.js

@ -0,0 +1,79 @@
/*
* Copyright © 2016-2018 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.
*/
/* eslint-disable import/no-unresolved, import/default */
import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function RuleNodeDirective($compile, $templateCache, ruleChainService, types) {
var linker = function (scope, element) {
var template = $templateCache.get(ruleNodeFieldsetTemplate);
element.html(template);
scope.types = types;
scope.params = {
targetRuleChainId: null
};
scope.$watch('ruleNode', function() {
if (scope.ruleNode && scope.ruleNode.nodeType == types.ruleNodeType.RULE_CHAIN.value) {
scope.params.targetRuleChainId = scope.ruleNode.targetRuleChainId;
watchTargetRuleChain();
} else {
if (scope.targetRuleChainWatch) {
scope.targetRuleChainWatch();
scope.targetRuleChainWatch = null;
}
}
});
function watchTargetRuleChain() {
scope.targetRuleChainWatch = scope.$watch('params.targetRuleChainId',
function(targetRuleChainId) {
if (scope.ruleNode.targetRuleChainId != targetRuleChainId) {
scope.ruleNode.targetRuleChainId = targetRuleChainId;
if (targetRuleChainId) {
ruleChainService.getRuleChain(targetRuleChainId).then(
(ruleChain) => {
scope.ruleNode.name = ruleChain.name;
}
);
} else {
scope.ruleNode.name = "";
}
}
}
);
}
$compile(element.contents())(scope);
}
return {
restrict: "E",
link: linker,
scope: {
ruleChainId: '=',
ruleNode: '=',
isEdit: '=',
isReadOnly: '=',
theForm: '=',
onDeleteRuleNode: '&'
}
};
}

45
ui/src/app/rulechain/rulenode.tpl.html

@ -0,0 +1,45 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<div
id="{{node.id}}"
ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
ng-dblclick="callbacks.doubleClick($event, node)">
<div class="tb-rule-node {{node.nodeClass}}">
<md-icon aria-label="node-type-icon" flex="15"
class="material-icons">{{node.icon}}</md-icon>
<div layout="column" flex="85" layout-align="center">
<span class="tb-node-type">{{ node.type }}</span>
<span class="tb-node-title" ng-if="node.name">{{ node.name }}</span>
</div>
<div class="{{flowchartConstants.leftConnectorClass}}">
<div fc-magnet
ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.leftConnectorType)">
<div fc-connector></div>
</div>
</div>
<div class="{{flowchartConstants.rightConnectorClass}}">
<div fc-magnet
ng-repeat="connector in modelservice.nodes.getConnectorsByType(node, flowchartConstants.rightConnectorType)">
<div fc-connector></div>
</div>
</div>
</div>
<div ng-if="modelservice.isEditable() && !node.readonly" class="fc-nodedelete" ng-click="modelservice.nodes.delete(node)">
&times;
</div>
</div>
Loading…
Cancel
Save