소스 검색

Integration of the new way to compute relations into the datamodel (ComputeImpactedItems)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3570 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 10 년 전
부모
커밋
2c4841693c

+ 38 - 2
core/dbobject.class.php

@@ -2546,7 +2546,7 @@ abstract class DBObject implements iDisplay
 	}
 
 	/**
-	 * Will be deprecated soon - use MetaModel::GetRelatedObjectsDown/Up instead to take redundancy into account
+	 * Will be deprecated soon - use GetRelatedObjectsDown/Up instead to take redundancy into account
 	 */
 	public function GetRelatedObjects($sRelCode, $iMaxDepth = 99, &$aResults = array())
 	{
@@ -2608,7 +2608,43 @@ abstract class DBObject implements iDisplay
 		}
 		return $aResults;
 	}
-
+	
+	/**
+	 * Compute the "RelatedObjects" (forward or "down" direction) for the object
+	 * for the specified relation
+	 *
+	 * @param string $sRelCode The code of the relation to use for the computation
+	 * @param int $iMaxDepth Maximum recursion depth
+	 * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy
+	 *
+	 * @return RelationGraph The graph of all the related objects
+	 */
+	public function GetRelatedObjectsDown($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true)
+	{
+		$oGraph = new RelationGraph();
+		$oGraph->AddSourceObject($this);
+		$oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy);
+		return $oGraph;
+	}
+	
+	/**
+	 * Compute the "RelatedObjects" (reverse or "up" direction) for the object
+	 * for the specified relation
+	 *
+	 * @param string $sRelCode The code of the relation to use for the computation
+	 * @param int $iMaxDepth Maximum recursion depth
+	 * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy
+	 *
+	 * @return RelationGraph The graph of all the related objects
+	 */
+	public function GetRelatedObjectsUp($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true)
+	{
+		$oGraph = new RelationGraph();
+		$oGraph->AddSourceObject($this);
+		$oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy);
+		return $oGraph;
+	}
+	
 	public function GetReferencingObjects($bAllowAllData = false)
 	{
 		$aDependentObjects = array();

+ 44 - 0
core/dbobjectset.class.php

@@ -993,6 +993,50 @@ class DBObjectSet
 	}
 
 	/**
+	 * Compute the "RelatedObjects" (forward or "down" direction) for the set
+	 * for the specified relation
+	 *
+	 * @param string $sRelCode The code of the relation to use for the computation
+	 * @param int $iMaxDepth Maximum recursion depth
+	 * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy
+	 *
+	 * @return RelationGraph The graph of all the related objects
+	 */
+	public function GetRelatedObjectsDown($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true)
+	{
+		$oGraph = new RelationGraph();
+		$this->Rewind();
+		while($oObj = $this->Fetch())
+		{
+			$oGraph->AddSourceObject($oObj);
+		}
+		$oGraph->ComputeRelatedObjectsDown($sRelCode, $iMaxDepth, $bEnableRedundancy);
+		return $oGraph;
+	}
+	
+	/**
+	 * Compute the "RelatedObjects" (reverse or "up" direction) for the set
+	 * for the specified relation
+	 *
+	 * @param string $sRelCode The code of the relation to use for the computation
+	 * @param int $iMaxDepth Maximum recursion depth
+	 * @param boolean $bEnableReduncancy Whether or not to take into account the redundancy
+	 *
+	 * @return RelationGraph The graph of all the related objects
+	 */
+	public function GetRelatedObjectsUp($sRelCode, $iMaxDepth = 99, $bEnableRedundancy = true)
+	{
+		$oGraph = new RelationGraph();
+		$this->Rewind();
+		while($oObj = $this->Fetch())
+		{
+			$oGraph->AddSinkObject($oObj);
+		}
+		$oGraph->ComputeRelatedObjectsUp($sRelCode, $iMaxDepth, $bEnableRedundancy);
+		return $oGraph;
+	}
+	
+	/**
 	 * Builds an object that contains the values that are common to all the objects
 	 * in the set. If for a given attribute, objects in the set have various values
 	 * then the resulting object will contain null for this value.

+ 24 - 0
core/displayablegraph.class.inc.php

@@ -88,6 +88,8 @@ class DisplayableNode extends GraphNode
 		$aNode['icon_url'] = $this->GetIconURL();
 		$aNode['width'] = 32;
 		$aNode['source'] = ($this->GetProperty('source') == true);
+		$aNode['obj_class'] = get_class($this->GetProperty('object'));
+		$aNode['obj_key'] = $this->GetProperty('object')->GetKey();
 		$aNode['sink'] = ($this->GetProperty('sink') == true);
 		$aNode['x'] = $this->x;
 		$aNode['y']= $this->y;
@@ -291,6 +293,7 @@ class DisplayableNode extends GraphNode
 							if ($oGraph->GetNode($oNode->GetId()))
 							{
 								$oGraph->_RemoveNode($oNode);
+								$oNewNode->AddObject($oNode->GetProperty('object'));
 							}
 						}
 						$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
@@ -412,6 +415,7 @@ class DisplayableRedundancyNode extends DisplayableNode
 							}
 //echo "<p>Replacing ".$oNode->GetId().' by '.$oNewNode->GetId()."\n";
 							$oGraph->_RemoveNode($oNode);
+							$oNewNode->AddObject($oNode->GetProperty('object'));
 						}
 						//$oNewNode->GroupSimilarNeighbours($oGraph, $iThresholdCount, $bDirectionUp, $bDirectionDown);
 					}
@@ -471,6 +475,24 @@ class DisplayableEdge extends GraphEdge
 
 class DisplayableGroupNode extends DisplayableNode
 {
+	protected $aObjects;
+	
+	public function __construct(SimpleGraph $oGraph, $sId, $x = 0, $y = 0)
+	{
+		parent::__construct($oGraph, $sId, $x, $y);
+		$this->aObjects = array();
+	}
+	
+	public function AddObject(DBObject $oObj)
+	{
+		$this->aObjects[$oObj->GetKey()] = $oObj;
+	}
+	
+	public function GetObjects()
+	{
+		return $this->aObjects;
+	}
+	
 	public function GetWidth()
 	{
 		return 50;
@@ -487,6 +509,7 @@ class DisplayableGroupNode extends DisplayableNode
 		$aNode['y']= $this->y;
 		$aNode['label'] = $this->GetLabel();
 		$aNode['id'] = $this->GetId();
+		$aNode['group_index'] = $this->GetProperty('group_index'); // if supplied
 		$fDiscOpacity = ($this->GetProperty('is_reached') ? 1 : 0.2);
 		$fTextOpacity = ($this->GetProperty('is_reached') ? 1 : 0.4);
 		$aNode['icon_attr'] = array('opacity' => $fTextOpacity);
@@ -583,6 +606,7 @@ class DisplayableGraph extends SimpleGraph
 				}
 				$oObj = $oNode->GetProperty('object');
 				$oNewNode->SetProperty('class', get_class($oObj));
+				$oNewNode->SetProperty('object', $oObj);
 				$oNewNode->SetProperty('icon_url', $oObj->GetIcon(false));
 				$oNewNode->SetProperty('label', $oObj->GetRawName());
 				$oNewNode->SetProperty('is_reached', $bDirectionDown ? $oNode->GetProperty('is_reached') : true); // When going "up" is_reached does not matter

+ 1 - 0
core/metamodel.class.php

@@ -21,6 +21,7 @@ require_once(APPROOT.'core/querybuildercontext.class.inc.php');
 require_once(APPROOT.'core/querymodifier.class.inc.php');
 require_once(APPROOT.'core/metamodelmodifier.inc.php');
 require_once(APPROOT.'core/computing.inc.php');
+require_once(APPROOT.'core/relationgraph.class.inc.php');
 
 /**
  * Metamodel

+ 93 - 49
core/restservices.class.inc.php

@@ -417,30 +417,108 @@ class CoreServices implements iRestServiceProvider
 			$key = RestUtils::GetMandatoryParam($aParams, 'key');
 			$sRelation = RestUtils::GetMandatoryParam($aParams, 'relation');
 			$iMaxRecursionDepth = RestUtils::GetOptionalParam($aParams, 'depth', 20 /* = MAX_RECURSION_DEPTH */);
+			$sDirection = RestUtils::GetOptionalParam($aParams, 'direction', null);
+			$bEnableRedundancy = RestUtils::GetOptionalParam($aParams, 'redundancy', false);
+			$bReverse = false;
+			
+			if (is_null($sDirection) && ($sRelation == 'depends on'))
+			{
+				// Legacy behavior, consider "depends on" as a forward relation
+				$sRelation = 'impacts';
+				$sDirection = 'up'; 
+				$bReverse = true; // emulate the legacy behavior by returning the edges
+			}
+			else if(is_null($sDirection))
+			{
+				$sDirection = 'down';
+			}
 	
 			$oObjectSet = RestUtils::GetObjectSetFromKey($sClass, $key);
+			if ($sDirection == 'down')
+			{
+				$oRelationGraph = $oObjectSet->GetRelatedObjectsDown($sRelation, $iMaxRecursionDepth, $bEnableRedundancy);
+			}
+			else if ($sDirection == 'up')
+			{
+				$oRelationGraph = $oObjectSet->GetRelatedObjectsUp($sRelation, $iMaxRecursionDepth, $bEnableRedundancy);
+			}
+			else
+			{
+				$oResult->code = RestResult::INTERNAL_ERROR;
+				$oResult->message = "Invalid value: '$sDirection' for the parameter 'direction'. Valid values are 'up' and 'down'";
+				return $oResult;
+				
+			}
+			
+			if ($bEnableRedundancy)
+			{
+				// Remove the redundancy nodes from the output
+				$oIterator = new RelationTypeIterator($oRelationGraph, 'Node');
+				foreach($oIterator as $oNode)
+				{
+					if ($oNode instanceof RelationRedundancyNode)
+					{
+						$oRelationGraph->FilterNode($oNode);
+					}
+				}
+			}
+			
 			$aIndexByClass = array();
-			while ($oObject = $oObjectSet->Fetch())
+			$oIterator = new RelationTypeIterator($oRelationGraph);
+			foreach($oIterator as $oElement)
 			{
-				$aRelated = array();
-				$aGraph = array();
-				$aIndexByClass[get_class($oObject)][$oObject->GetKey()] = null;
-				$oResult->AddObject(0, '', $oObject);
-				$this->GetRelatedObjects($oObject, $sRelation, $iMaxRecursionDepth, $aRelated, $aGraph);
-	
-				foreach($aRelated as $sClass => $aObjects)
+				if ($oElement instanceof RelationObjectNode)
 				{
-					foreach($aObjects as $oRelatedObj)
+					$oObject = $oElement->GetProperty('object');
+					if ($oObject)
 					{
-						$aIndexByClass[get_class($oRelatedObj)][$oRelatedObj->GetKey()] = null;
-						$oResult->AddObject(0, '', $oRelatedObj);
-					}				
+						if ($bEnableRedundancy)
+						{
+							// Add only the "reached" objects
+							if ($oElement->GetProperty('is_reached'))
+							{
+								$aIndexByClass[get_class($oObject)][$oObject->GetKey()] = null;
+								$oResult->AddObject(0, '', $oObject);
+							}
+						}
+						else
+						{
+							$aIndexByClass[get_class($oObject)][$oObject->GetKey()] = null;
+							$oResult->AddObject(0, '', $oObject);
+						}
+					}
 				}
-				foreach($aGraph as $sSrcKey => $aDestinations)
+				else if ($oElement instanceof RelationEdge)
 				{
-					foreach ($aDestinations as $sDestKey)
+					$oSrcObj = $oElement->GetSourceNode()->GetProperty('object');
+					$oDestObj = $oElement->GetSinkNode()->GetProperty('object');
+					$sSrcKey = get_class($oSrcObj).'::'.$oSrcObj->GetKey();
+					$sDestKey = get_class($oDestObj).'::'.$oDestObj->GetKey();
+					if ($bEnableRedundancy)
 					{
-						$oResult->AddRelation($sSrcKey, $sDestKey);
+						// Add only the edges where both source and destination are "reached"
+						if ($oElement->GetSourceNode()->GetProperty('is_reached') && $oElement->GetSinkNode()->GetProperty('is_reached'))
+						{
+							if ($bReverse)
+							{
+								$oResult->AddRelation($sDestKey, $sSrcKey);
+							}
+							else
+							{
+								$oResult->AddRelation($sSrcKey, $sDestKey);
+							}
+						}
+					}
+					else
+					{
+						if ($bReverse)
+						{
+							$oResult->AddRelation($sDestKey, $sSrcKey);
+						}
+						else
+						{
+							$oResult->AddRelation($sSrcKey, $sDestKey);
+						}
 					}
 				}
 			}
@@ -606,38 +684,4 @@ class CoreServices implements iRestServiceProvider
 			$oResult->message = $sRes;
 		}
 	}
-	
-	/**
-	 * Helper function to get the related objects up to the given depth along with the "graph" of the relation
-	 * @param DBObject $oObject Starting point of the computation
-	 * @param string $sRelation Code of the relation (i.e; 'impact', 'depends on'...)
-	 * @param integer $iMaxRecursionDepth Maximum level of recursion
-	 * @param Hash $aRelated Two dimensions hash of the already related objects: array( 'class' => array(key => ))
-	 * @param Hash	$aGraph Hash array for the topology of the relation: source => related: array('class:key' => array( DBObjects ))
-	 * @param integer $iRecursionDepth Current level of recursion
-	 */
-	protected function GetRelatedObjects(DBObject $oObject, $sRelation, $iMaxRecursionDepth, &$aRelated, &$aGraph, $iRecursionDepth = 1)
-	{
-		// Avoid loops
-		if ((array_key_exists(get_class($oObject), $aRelated)) && (array_key_exists($oObject->GetKey(), $aRelated[get_class($oObject)]))) return;
-		// Stop at maximum recursion level
-		if ($iRecursionDepth > $iMaxRecursionDepth) return;
-		
-		$sSrcKey = get_class($oObject).'::'.$oObject->GetKey();
-		$aNewRelated = array();
-		$oObject->GetRelatedObjects($sRelation, 1, $aNewRelated);
-		foreach($aNewRelated as $sClass => $aObjects)
-		{
-			if (!array_key_exists($sSrcKey, $aGraph))
-			{
-				$aGraph[$sSrcKey] = array();
-			}
-			foreach($aObjects as $oRelatedObject)
-			{
-				$aRelated[$sClass][$oRelatedObject->GetKey()] = $oRelatedObject;
-				$aGraph[$sSrcKey][] = get_class($oRelatedObject).'::'.$oRelatedObject->GetKey();
-				$this->GetRelatedObjects($oRelatedObject, $sRelation, $iMaxRecursionDepth, $aRelated, $aGraph, $iRecursionDepth+1);
-			}
-		}
-	}
 }

+ 39 - 3
core/simplegraph.class.inc.php

@@ -338,6 +338,41 @@ class SimpleGraph
 	}
 	
 	/**
+	 * Removes the given node but preserves the connectivity of the graph
+	 * all "source" nodes are connected to all "sink" nodes
+	 * @param GraphNode $oNode
+	 * @throws SimpleGraphException
+	 */
+	public function FilterNode(GraphNode $oNode)
+	{
+		if (!array_key_exists($oNode->GetId(), $this->aNodes)) throw new SimpleGraphException('Cannot filter the node (id='.$oNode->GetId().') from the graph. The node was not found in the graph.');
+		
+		$aSourceNodes = array();
+		$aSinkNodes = array();
+		foreach($oNode->GetOutgoingEdges() as $oEdge)
+		{
+			$sSinkId =  $oEdge->GetSinkNode()->GetId();
+			$aSinkNodes[$sSinkId] = $oEdge->GetSinkNode();
+			$this->_RemoveEdge($oEdge);
+		}
+		foreach($oNode->GetIncomingEdges() as $oEdge)
+		{
+			$sSourceId =  $oEdge->GetSourceNode()->GetId();
+			$aSourceNodes[$sSourceId] = $oEdge->GetSourceNode();
+			$this->_RemoveEdge($oEdge);
+		}
+		unset($this->aNodes[$oNode->GetId()]);
+
+		foreach($aSourceNodes as $sSourceId => $oSourceNode)
+		{
+			foreach($aSinkNodes as $sSinkId => $oSinkNode)
+			{
+				$oEdge = new RelationEdge($this, $oSourceNode, $oSinkNode);
+			}
+		}
+	}
+	
+	/**
 	 * Get the node identified by $sId or null if not found
 	 * @param string $sId
 	 * @return NULL | GraphNode
@@ -499,9 +534,10 @@ EOF
 	}
 	
 	/**
-	 * Get the description of the graph as an embedded PNG image (using a data: url) as
-	 * generated by graphviz (requires graphviz to be installed on the machine and the path to
-	 * dot/dot.exe to be configured in the iTop configuration file)
+	 * Get the description of the graph as text string in the XDot format
+	 * including the positions of the nodes and egdes (requires graphviz
+	 * to be installed on the machine and the path to dot/dot.exe to be
+	 * configured in the iTop configuration file)
 	 * Note: the function creates temporary files in APPROOT/data/tmp
 	 * @return string
 	 */

+ 15 - 1
datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml

@@ -1368,7 +1368,21 @@
 
 		$oImpactedInfras = DBObjectSet::FromLinkSet($this, 'functionalcis_list', 'functionalci_id');
 
-		$aComputed = $oImpactedInfras->GetRelatedObjects('impacts', 10);
+        $oGraph = $oImpactedInfras->GetRelatedObjectsDown('impacts',10, true /* bEnableRedundancy */);
+        $oIterator = new RelationTypeIterator($oGraph, 'Node');
+        foreach($oIterator as $oNode)
+        {
+            if($oNode instanceof RelationObjectNode)
+            {
+                $oObj = $oNode->GetProperty('object');
+                $sRootClass = MetaModel::GetRootClass(get_class($oObj));
+                if (!array_key_exists($sRootClass, $aComputed))
+                {
+                    $aComputed[$sRootClass] = array();
+                }
+                $aComputed[$sRootClass][$oObj->GetKey()] = $oObj;
+            }
+        }
 
 		if (isset($aComputed['FunctionalCI']) && is_array($aComputed['FunctionalCI']))
 		{

+ 19 - 2
datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml

@@ -1370,8 +1370,25 @@
 
 		$oImpactedInfras = DBObjectSet::FromLinkSet($this, 'functionalcis_list', 'functionalci_id');
 
-		$aComputed = $oImpactedInfras->GetRelatedObjects('impacts', 10);
-
+        $oGraph = $oImpactedInfras->GetRelatedObjectsDown('impacts',10, true /* bEnableRedundancy */);
+        $oIterator = new RelationTypeIterator($oGraph, 'Node');
+        foreach($oIterator as $oNode)
+        {
+            if($oNode instanceof RelationObjectNode)
+            {
+                if ($oNode->GetProperty('is_reached'))
+                {
+                    $oObj = $oNode->GetProperty('object');
+                    $sRootClass = MetaModel::GetRootClass(get_class($oObj));
+                    if (!array_key_exists($sRootClass, $aComputed))
+                    {
+                        $aComputed[$sRootClass] = array();
+                    }
+                    $aComputed[$sRootClass][$oObj->GetKey()] = $oObj;
+                }
+            }
+        }
+        
 		if (isset($aComputed['FunctionalCI']) && is_array($aComputed['FunctionalCI']))
 		{
 			foreach($aComputed['FunctionalCI'] as $iKey => $oObject)

+ 71 - 2
js/simple_graph.js

@@ -43,6 +43,7 @@ $(function()
 			.addClass('graph');
 			
 			this._create_toolkit_menu();
+			this._build_context_menus();
 		},
 	
 		// called when created, and later when changing options
@@ -134,13 +135,13 @@ $(function()
 				oNode.aElements.push(this.oPaper.image(oNode.icon_url, xPos - iWidth * this.fZoom/2, yPos - iHeight * this.fZoom/2, iWidth*this.fZoom, iHeight*this.fZoom).attr(oNode.icon_attr));
 				var oText = this.oPaper.text( xPos, yPos, oNode.label);
 				oText.attr(oNode.text_attr);
-				oText.transform('s'+this.fZoom);
+				oText.transform('S'+this.fZoom);
 				var oBB = oText.getBBox();
 				var dy = iHeight/2*this.fZoom + oBB.height/2;
 				oText.remove();
 				oText = this.oPaper.text( xPos, yPos + dy, oNode.label);
 				oText.attr(oNode.text_attr);
-				oText.transform('s'+this.fZoom);
+				oText.transform('S'+this.fZoom);
 				oNode.aElements.push(oText);
 				oNode.aElements.push(this.oPaper.rect( xPos - oBB.width/2 -2, yPos - oBB.height/2 + dy, oBB.width +4, oBB.height).attr({fill: '#fff', stroke: '#fff', opacity: 0.9}).toBack());
 				break;
@@ -158,6 +159,7 @@ $(function()
 			for(k in oNode.aElements)
 			{
 				var sNodeId = oNode.id;
+				$(oNode.aElements[k].node).attr({'data-type': oNode.shape, 'data-id': oNode.id} ).attr('class', 'popupMenuTarget');
 				oNode.aElements[k].drag(function(dx, dy, x, y, event) { me._move(sNodeId, dx, dy, x, y, event); }, function(x, y, event) { me._drag_start(sNodeId, x, y, event); }, function (event) { me._drag_end(sNodeId, event); });
 			}
 		},
@@ -309,6 +311,16 @@ $(function()
 			oEdge.aElements = [];
 			this.aEdges.push(oEdge);
 		},
+		show_group: function(sGroupId)
+		{
+			// Activate the 3rd tab
+			this.element.closest('.ui-tabs').tabs("option", "active", 2);
+			// Scroll into view the group
+			if ($('#'+sGroupId).length > 0)
+			{
+				$('#'+sGroupId)[0].scrollIntoView();				
+			}
+		},
 		_create_toolkit_menu: function()
 		{
 			var sPopupMenuId = 'tk_graph'+this.element.attr('id');
@@ -334,6 +346,63 @@ $(function()
 			$('#'+sPopupMenuId+'_reload').click(function() { me.reload(); });
 			
 		},
+		_build_context_menus: function()
+		{
+			var sId = this.element.attr('id');
+			var me = this;
+			
+			$.contextMenu({
+			    selector: '#'+sId+' .popupMenuTarget',  
+		        build: function(trigger, e) {
+		            // this callback is executed every time the menu is to be shown
+		            // its results are destroyed every time the menu is hidden
+		            // e is the original contextmenu event, containing e.pageX and e.pageY (amongst other data)
+		        	var sType = trigger.attr('data-type');
+		        	var sNodeId = trigger.attr('data-id');
+		        	var oNode = me._find_node(sNodeId);
+		        	
+		        	/*
+		        	var sObjName = trigger.attr('data-class');
+		        	var sIndex = trigger.attr('data-index');
+		        	var originalEvent = e;
+		        	var bHasItems = false;
+		        	*/
+		        	var oResult = {callback: null, items: {}};
+		        	switch(sType)
+		        	{
+		        		case 'group':
+		        		var sGroupIndex = oNode.group_index;
+		        		oResult = {
+	        				callback: function(key, options) {
+	        					var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
+	        					me.show_group('relation_group_'+sGroupIndex);
+	        				},
+	        				items: { 'show': {name: 'Show group' } }
+	        			};
+						break;
+						
+		        		case 'icon':
+			        	var sObjClass = oNode.obj_class;
+		        		var sObjKey = oNode.obj_key;
+		        		oResult = {
+	        				callback: function(key, options) {
+	        					var me = $('.itop-simple-graph').data('itopSimple_graph'); // need a live value
+	        					var sURL = me.options.drill_down_url.replace('%1$s', sObjClass).replace('%2$s', sObjKey);
+	        					window.location.href = sURL;
+	        				},
+	        				items: { 'details': {name: 'Show Details' } }
+	        			};
+		        		break;
+		        		
+		        		default:
+						oResult = false; // No context menu
+		        	
+		        	}
+		        	return oResult;
+		        }
+			});
+			
+		},
 		export_as_pdf: function()
 		{
 			var oPositions = {};

+ 51 - 9
pages/UI.php

@@ -224,11 +224,11 @@ function DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj)
 {
 	$oP->SetCurrentTab(Dict::S('UI:RelationshipList'));
 	$oP->add("<div id=\"impacted_objects\" style=\"width:100%;background-color:#fff;padding:10px;\">");
+	$oP->add("<h1>".MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName()."</h1>\n");
 	$iBlock = 1; // Zero is not a valid blockid
 	foreach($aResults as $sListClass => $aObjects)
 	{
 		$oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
-		$oP->add("<h1>".MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName()."</h1>\n");
 		$oP->add("<div class=\"page_header\">\n");
 		$oP->add("<h2>".MetaModel::GetClassIcon($sListClass)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aObjects), Metamodel::GetName($sListClass))."</h2>\n");
 		$oP->add("</div>\n");
@@ -239,7 +239,30 @@ function DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj)
 	$oP->add("</div>");
 }
 
-function DisplayNavigatorGraphicsTab($oP, $aResults, $oRelGraph, $sClass, $id, $sRelation, $oAppContext, $bDirectionDown)
+function DisplayNavigatorGroupTab($oP, $aGroups, $sRelation, $oObj)
+{
+	if (count($aGroups) > 0)
+	{
+		$oP->SetCurrentTab(Dict::S('UI:RelationGroups'));
+		$oP->add("<div id=\"impacted_groupss\" style=\"width:100%;background-color:#fff;padding:10px;\">");
+		$iBlock = 1; // Zero is not a valid blockid
+		foreach($aGroups as $idx => $aObjects)
+		{
+			$sListClass = get_class(current($aObjects));
+			$oSet = CMDBObjectSet::FromArray($sListClass, $aObjects);
+			$oP->add("<h1>".Dict::Format('UI:RelationGroupNumber_N', (1+$idx))."</h1>\n");
+			$oP->add("<div id=\"relation_group_$idx\" class=\"page_header\">\n");
+			$oP->add("<h2>".MetaModel::GetClassIcon($sListClass)."&nbsp;<span class=\"hilite\">".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aObjects), Metamodel::GetName($sListClass))."</h2>\n");
+			$oP->add("</div>\n");
+			$oBlock = DisplayBlock::FromObjectSet($oSet, 'list');
+			$oBlock->Display($oP, 'group_'.$iBlock++);
+			$oP->p('&nbsp;'); // Some space ?
+		}
+		$oP->add("</div>");
+	}
+}
+
+function DisplayNavigatorGraphicsTab($oP, $aResults, $oDisplayGraph, $sClass, $id, $sRelation, $oAppContext, $bDirectionDown)
 {
 	$oP->SetCurrentTab(Dict::S('UI:RelationshipGraph'));
 
@@ -279,11 +302,12 @@ EOF
 	$iGroupingThreshold = utils::ReadParam('g', 5);
 		
 	$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/fraphael.js');
+	$oP->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/jquery.contextMenu.css');
+	$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.contextMenu.js');
 	$oP->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/simple_graph.js');
-	$oGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, $bDirectionDown);
 	try
 	{
-		$oGraph->InitFromGraphviz();
+		$oDisplayGraph->InitFromGraphviz();
 		$sExportAsPdfURL = '';
 		if (extension_loaded('gd'))
 		{
@@ -294,7 +318,7 @@ EOF
 		$sDrillDownURL = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=details&class=%1$s&id=%2$s&'.$sContext;
 		$sExportAsDocumentURL = '';
 		
-		$oGraph->RenderAsRaphael($oP, null, $sExportAsPdfURL, $sExportAsDocumentURL, $sDrillDownURL);
+		$oDisplayGraph->RenderAsRaphael($oP, null, $sExportAsPdfURL, $sExportAsDocumentURL, $sDrillDownURL);
 	}
 	catch(Exception $e)
 	{
@@ -1531,7 +1555,8 @@ EOF
 		$id = utils::ReadParam('id', 0);
 		$sRelation = utils::ReadParam('relation', 'impact');
 		$sDirection = utils::ReadParam('direction', 'down');
-
+		$iGroupingThreshold = utils::ReadParam('g', 5);
+		
 		$oObj = MetaModel::GetObject($sClass, $id);
 		$iMaxRecursionDepth = MetaModel::GetConfig()->Get('relations_max_depth', 20);
 		$aSourceObjects = array($oObj);
@@ -1551,10 +1576,12 @@ EOF
 		
 
 		$aResults = array();
+		$aGroups = array();
+		$iGroupIdx = 0;
 		$oIterator = new RelationTypeIterator($oRelGraph, 'Node');
 		foreach($oIterator as $oNode)
 		{
-			$oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes) do not contain an object
+			$oObj = $oNode->GetProperty('object'); // Some nodes (Redundancy Nodes and Group) do not contain an object
 			if ($oObj)
 			{
 				$sObjClass  = get_class($oObj);
@@ -1565,6 +1592,19 @@ EOF
 				$aResults[$sObjClass][] = $oObj;
 			}
 		}
+
+		$oDisplayGraph = DisplayableGraph::FromRelationGraph($oRelGraph, $iGroupingThreshold, ($sDirection == 'down'));
+		
+		$oIterator = new RelationTypeIterator($oDisplayGraph, 'Node');
+		foreach($oIterator as $oNode)
+		{
+			if ($oNode instanceof DisplayableGroupNode)
+			{
+				$aGroups[] = $oNode->GetObjects();
+				$oNode->SetProperty('group_index', $iGroupIdx);
+				$iGroupIdx++;
+			}
+		}		
 		
 		$oP->AddTabContainer('Navigator');
 		$oP->SetCurrentTabContainer('Navigator');
@@ -1573,12 +1613,14 @@ EOF
 		if ($sFirstTab == 'list')
 		{
 			DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj);
-			DisplayNavigatorGraphicsTab($oP, $aResults, $oRelGraph, $sClass, $id, $sRelation, $oAppContext, ($sDirection == 'down'));
+			DisplayNavigatorGraphicsTab($oP, $aResults, $oDisplayGraph, $sClass, $id, $sRelation, $oAppContext, ($sDirection == 'down'));
+			DisplayNavigatorGroupTab($oP, $aGroups, $sRelation, $oObj);
 		}
 		else
 		{
-			DisplayNavigatorGraphicsTab($oP, $aResults, $oRelGraph, $sClass, $id, $sRelation, $oAppContext, ($sDirection == 'down'));
+			DisplayNavigatorGraphicsTab($oP, $aResults, $oDisplayGraph, $sClass, $id, $sRelation, $oAppContext, ($sDirection == 'down'));
 			DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj);
+			DisplayNavigatorGroupTab($oP, $aGroups, $sRelation, $oObj);
 		}
 
 		$oP->SetCurrentTab('');