diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 054bb523e..8b4f1a180 100644 --- a/core/dbobject.class.php +++ b/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(); diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php index 1fae8ff86..a2d4326c8 100644 --- a/core/dbobjectset.class.php +++ b/core/dbobjectset.class.php @@ -992,6 +992,50 @@ class DBObjectSet return $aRelatedObjs; } + /** + * 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 diff --git a/core/displayablegraph.class.inc.php b/core/displayablegraph.class.inc.php index ed544d5c5..06497954f 100644 --- a/core/displayablegraph.class.inc.php +++ b/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 "

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 diff --git a/core/metamodel.class.php b/core/metamodel.class.php index e465675e8..ac4103435 100644 --- a/core/metamodel.class.php +++ b/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 diff --git a/core/restservices.class.inc.php b/core/restservices.class.inc.php index f7f4b55cf..71c03dd2a 100644 --- a/core/restservices.class.inc.php +++ b/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); - $aIndexByClass = array(); - while ($oObject = $oObjectSet->Fetch()) + if ($sDirection == 'down') { - $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) + $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) { - foreach($aObjects as $oRelatedObj) + if ($oNode instanceof RelationRedundancyNode) { - $aIndexByClass[get_class($oRelatedObj)][$oRelatedObj->GetKey()] = null; - $oResult->AddObject(0, '', $oRelatedObj); - } + $oRelationGraph->FilterNode($oNode); + } } - foreach($aGraph as $sSrcKey => $aDestinations) + } + + $aIndexByClass = array(); + $oIterator = new RelationTypeIterator($oRelationGraph); + foreach($oIterator as $oElement) + { + if ($oElement instanceof RelationObjectNode) { - foreach ($aDestinations as $sDestKey) + $oObject = $oElement->GetProperty('object'); + if ($oObject) { - $oResult->AddRelation($sSrcKey, $sDestKey); + 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); + } + } + } + else if ($oElement instanceof RelationEdge) + { + $oSrcObj = $oElement->GetSourceNode()->GetProperty('object'); + $oDestObj = $oElement->GetSinkNode()->GetProperty('object'); + $sSrcKey = get_class($oSrcObj).'::'.$oSrcObj->GetKey(); + $sDestKey = get_class($oDestObj).'::'.$oDestObj->GetKey(); + if ($bEnableRedundancy) + { + // 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); - } - } - } } diff --git a/core/simplegraph.class.inc.php b/core/simplegraph.class.inc.php index 71d3f09be..49e5ec0b2 100644 --- a/core/simplegraph.class.inc.php +++ b/core/simplegraph.class.inc.php @@ -337,6 +337,41 @@ class SimpleGraph unset($this->aNodes[$oNode->GetId()]); } + /** + * 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 @@ -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 */ diff --git a/datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml b/datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml index cf1c3018f..955cb551e 100755 --- a/datamodels/2.x/itop-request-mgmt-itil/datamodel.itop-request-mgmt-itil.xml +++ b/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'])) { diff --git a/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml b/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml index ec5b444ce..caa4e7504 100755 --- a/datamodels/2.x/itop-request-mgmt/datamodel.itop-request-mgmt.xml +++ b/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) diff --git a/js/simple_graph.js b/js/simple_graph.js index 71fb6bd6a..fbe743216 100644 --- a/js/simple_graph.js +++ b/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'); @@ -333,6 +345,63 @@ $(function() $('#'+sPopupMenuId+'_document').click(function() { me.export_as_document(); }); $('#'+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() { diff --git a/pages/UI.php b/pages/UI.php index cb89bab6b..1414d4511 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -224,11 +224,11 @@ function DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj) { $oP->SetCurrentTab(Dict::S('UI:RelationshipList')); $oP->add("

"); + $oP->add("

".MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName()."

\n"); $iBlock = 1; // Zero is not a valid blockid foreach($aResults as $sListClass => $aObjects) { $oSet = CMDBObjectSet::FromArray($sListClass, $aObjects); - $oP->add("

".MetaModel::GetRelationDescription($sRelation).' '.$oObj->GetName()."

\n"); $oP->add("
\n"); $oP->add("

".MetaModel::GetClassIcon($sListClass)." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aObjects), Metamodel::GetName($sListClass))."

\n"); $oP->add("
\n"); @@ -239,7 +239,30 @@ function DisplayNavigatorListTab($oP, $aResults, $sRelation, $oObj) $oP->add("
"); } -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("
"); + $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("

".Dict::Format('UI:RelationGroupNumber_N', (1+$idx))."

\n"); + $oP->add("
\n"); + $oP->add("

".MetaModel::GetClassIcon($sListClass)." ".Dict::Format('UI:Search:Count_ObjectsOf_Class_Found', count($aObjects), Metamodel::GetName($sListClass))."

\n"); + $oP->add("
\n"); + $oBlock = DisplayBlock::FromObjectSet($oSet, 'list'); + $oBlock->Display($oP, 'group_'.$iBlock++); + $oP->p(' '); // Some space ? + } + $oP->add("
"); + } +} + +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('');