ui-modules/blueprint-composer/app/components/designer/designer.directive.js (236 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import angular from 'angular';
import {Entity} from "../util/model/entity.model";
import {D3Blueprint} from "../util/d3-blueprint";
import {EntityFamily} from '../util/model/entity.model';
import {graphicalEditEntityState} from '../../views/main/graphical/edit/entity/edit.entity.controller';
import {graphicalEditSpecState} from '../../views/main/graphical/edit/spec/edit.spec.controller';
import {graphicalEditPolicyState} from '../../views/main/graphical/edit/policy/edit.policy.controller';
import {graphicalEditEnricherState} from '../../views/main/graphical/edit/enricher/edit.enricher.controller';
const MODULE_NAME = 'brooklyn.components.designer';
const TEMPLATE_URL = 'blueprint-composer/component/designer/index.html';
const TAG = 'DIRECTIVE :: DESIGNER :: ';
angular.module(MODULE_NAME, [])
.directive('designer', ['$log', '$state', '$q', '$rootScope', 'iconGenerator', 'catalogApi', 'blueprintService', 'brSnackbar', 'paletteDragAndDropService', 'composerOverrides', designerDirective])
.run(['$templateCache', templateCache]);
export default MODULE_NAME;
export function designerDirective($log, $state, $q, $rootScope, iconGenerator, catalogApi, blueprintService, brSnackbar, paletteDragAndDropService, composerOverrides) {
return {
restrict: 'E',
templateUrl: function (tElement, tAttrs) {
return tAttrs.templateUrl || TEMPLATE_URL;
},
scope: {
onSelectionChange: '<?'
},
link: link
};
function link($scope, $element) {
let container = $element[0];
let blueprintGraph = new D3Blueprint(container, {shouldShowNode: composerOverrides.shouldShowNode}).center();
// allow downstream to configure this directive and/or scope
(composerOverrides.configureDesignerDirective || function () {})($scope, $element, blueprintGraph);
$scope.blueprint = blueprintService.get();
blueprintService.refreshBlueprintMetadata().then(() => {
redrawGraph();
// Start watching blueprint changes after metadata is refreshed. Metadata is changed many times while being
// refreshed, no need to re-draw on every change.
$scope.$watch('blueprint', () => {
redrawGraph();
}, true);
// Broadcast 'd3.metadata-refreshed' event, allow downstream to react on this event.
$scope.$broadcast('d3.metadata-refreshed');
});
$scope.selectedEntity = null;
$scope.$on('d3.redraw', (event, initial) => {
$log.debug(TAG + 'Re-draw blueprint, triggered by ' + event.name, initial, $scope.blueprint);
blueprintService.refreshBlueprintMetadata().then(() => {
redrawGraph();
if (initial) {
blueprintGraph.center();
}
});
});
$scope.$on('d3.remove', (event, entity) => {
$log.debug(TAG + `Delete ${entity.family.displayName} ${entity._id}`, entity);
let relationships = blueprintService.getRelationships().filter((relation) => (relation.target === entity));
switch (entity.family) {
case EntityFamily.ENTITY:
if (entity.hasParent()) {
$state.go(graphicalEditEntityState, { entityId: entity.parent._id })
}
entity.delete();
break;
case EntityFamily.POLICY:
entity.parent.removePolicy(entity._id);
break;
case EntityFamily.ENRICHER:
entity.parent.removeEnricher(entity._id);
break;
case EntityFamily.SPEC:
let memberSpecMap = entity.parent.getClusterMemberspecEntities();
Object.keys(memberSpecMap).forEach((key) => {
if (memberSpecMap[key] === entity) {
entity.parent.removeConfig(key);
}
});
break;
}
$q.all(relationships.map((relation) => (blueprintService.refreshRelationships(relation.source)))).then(() => {
$scope.$applyAsync(() => {
redrawGraph();
$state.go('main.graphical');
});
});
// Broadcast 'd3.removed' event, allow downstream to react on this event.
$scope.$broadcast('d3.removed');
});
$scope.$on('$stateChangeSuccess', (event, toState, toParams, fromState, fromParams, options) => {
let id;
switch (toState) {
case graphicalEditEntityState:
id = toParams.entityId;
break;
case graphicalEditSpecState:
id = toParams.specId;
break;
case graphicalEditPolicyState:
id = toParams.policyId;
break;
case graphicalEditEnricherState:
id = toParams.enricherId;
break;
}
if (angular.isDefined(id)) {
$log.debug(TAG + 'Select canvas, selected node: ' + id);
$scope.selectedEntity = blueprintService.findAny(id);
if ($scope.onSelectionChange) $scope.onSelectionChange($scope.selectedEntity);
}
});
$element.bind('click-svg', (event) => {
$log.debug(TAG + 'Select canvas, un-select node (if one selected before)');
$scope.selectedEntity = null;
if ($scope.onSelectionChange) $scope.onSelectionChange($scope.selectedEntity);
$scope.$apply(() => {
redrawGraph();
$state.go('main.graphical');
});
});
$element.bind('click-entity', (event) => {
$scope.$apply(() => {
const clickedEntity = event.detail.entity;
$log.debug(TAG + 'edit node ' + clickedEntity._id, clickedEntity);
switch (clickedEntity.family) {
case EntityFamily.ENTITY:
const blueprint = blueprintService.get();
if (blueprint.isInDslEdit) {
$rootScope.$broadcast('d3.entity-selected', clickedEntity);
blueprintGraph.hideShadow();
blueprintGraph.dropShadow(clickedEntity._id);
} else {
$state.go(graphicalEditEntityState, {entityId: clickedEntity._id});
}
break;
case EntityFamily.SPEC:
$state.go(graphicalEditSpecState, {entityId: clickedEntity.parent._id, specId: clickedEntity._id});
break;
case EntityFamily.POLICY:
$state.go(graphicalEditPolicyState, {entityId: clickedEntity.parent._id, policyId: clickedEntity._id});
break;
case EntityFamily.ENRICHER:
$state.go(graphicalEditEnricherState, {entityId: clickedEntity.parent._id, enricherId: clickedEntity._id});
break;
}
});
});
$element.bind('click-add-child', (event) => {
$log.debug(TAG + 'Add child to node ' + event.detail.entity._id);
$scope.$apply(() => {
$state.go('main.graphical.edit.add', {entityId: event.detail.entity._id, family: 'entity'});
});
});
$element.bind('move-entity', function (event) {
if (!event.detail.isNewParent) { // Do not remove, this event is intercepted in customized versions.
return;
}
let currentNode = blueprintService.find(event.detail.nodeId);
let parentNode = blueprintService.find(event.detail.parentId);
if (parentNode.hasAncestor(currentNode)) {
brSnackbar.create('Cannot move an entity node below itself or its own descendants');
} else {
$log.debug(TAG + 'move-entity ' + event.detail.nodeId, currentNode);
let targetIndex = event.detail.targetIndex;
if (currentNode.parent === parentNode && targetIndex > parentNode.children.indexOf(currentNode)) {
targetIndex--;
}
currentNode.parent.removeChild(currentNode._id);
if (targetIndex >= 0) {
parentNode.insertChild(currentNode, targetIndex);
}
else {
parentNode.addChild(currentNode);
}
}
blueprintService.refreshAllRelationships().then(()=> {
redrawGraph();
});
});
$element.bind('delete-entity', function (event) {
$log.debug('delete-entity');
$scope.$broadcast('d3.remove', event.detail.entity);
});
$element.bind('graph-redrawn', () => $scope.$root.$broadcast('layers.filter'));
$element.bind('drop-external-node', event => {
let draggedItem = paletteDragAndDropService.draggedItem();
let targetEntity = blueprintService.find(event.detail.parentId);
if (draggedItem.supertypes.includes(EntityFamily.ENTITY.superType)) {
let targetIndex = event.detail.targetIndex;
let newEntity = blueprintService.populateEntityFromApi(new Entity(), draggedItem);
if (targetIndex >= 0) {
targetEntity.insertChild(newEntity, targetIndex);
} else {
targetEntity.addChild(newEntity);
}
blueprintService.refreshEntityMetadata(newEntity, EntityFamily.ENTITY).then(() => {
container.dispatchEvent(new CustomEvent('new-entity-created', {
detail: {
nodeId: newEntity._id,
parentId: targetEntity._id
}
}));
$state.go(graphicalEditEntityState, {entityId: newEntity._id});
});
}
else if (draggedItem.supertypes.includes(EntityFamily.POLICY.superType)) {
let newPolicy = blueprintService.populateEntityFromApi(new Entity(), draggedItem);
targetEntity.addPolicy(newPolicy);
blueprintService.refreshEntityMetadata(newPolicy, EntityFamily.POLICY).then(() => {
$state.go(graphicalEditPolicyState, {entityId: targetEntity._id, policyId: newPolicy._id});
});
}
else if (draggedItem.supertypes.includes(EntityFamily.ENRICHER.superType)) {
let newEnricher = blueprintService.populateEntityFromApi(new Entity(), draggedItem);
targetEntity.addEnricher(newEnricher);
blueprintService.refreshEntityMetadata(newEnricher, EntityFamily.ENRICHER).then(() => {
$state.go(graphicalEditEnricherState, {entityId: targetEntity._id, enricherId: newEnricher._id});
});
}
else if (draggedItem.supertypes.includes(EntityFamily.LOCATION.superType)) {
blueprintService.populateLocationFromApi(targetEntity, draggedItem);
$state.go(graphicalEditEntityState, {entityId: targetEntity._id});
}
// Refresh relationships & redraw.
blueprintService.refreshAllRelationships().then(()=> {
redrawGraph();
});
});
function redrawGraph() {
let crossLinks = blueprintService.getRelationships();
blueprintGraph.update($scope.blueprint, crossLinks).draw();
if ($scope.selectedEntity) {
blueprintGraph.select($scope.selectedEntity._id);
} else {
blueprintGraph.unselect();
}
if (!$scope.blueprint.isInDslEdit) {
blueprintGraph.hideShadow();
}
}
}
}
function templateCache($templateCache) {
$templateCache.put(TEMPLATE_URL, '');
}