Ver código fonte

Data Exchange implementation in progress...

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@1104 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 14 anos atrás
pai
commit
43b39b8667

+ 34 - 35
application/cmdbabstract.class.inc.php

@@ -39,7 +39,40 @@ require_once(APPROOT.'/application/ui.passwordwidget.class.inc.php');
 require_once(APPROOT.'/application/ui.extkeywidget.class.inc.php');
 require_once(APPROOT.'/application/ui.htmleditorwidget.class.inc.php');
 
-abstract class cmdbAbstractObject extends CMDBObject
+/**
+ * All objects to be displayed in the application (either as a list or as details)
+ * must implement this interface.
+ */
+interface iDisplay
+{
+
+	/**
+	 * Maps the given context parameter name to the appropriate filter/search code for this class
+	 * @param string $sContextParam Name of the context parameter, i.e. 'org_id'
+	 * @return string Filter code, i.e. 'customer_id'
+	 */
+	public static function MapContextParam($sContextParam);
+	/**
+	 * This function returns a 'hilight' CSS class, used to hilight a given row in a table
+	 * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL,
+	 * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE
+	 * To Be overridden by derived classes
+	 * @param void
+	 * @return String The desired higlight class for the object/row
+	 */
+	public function GetHilightClass();
+	/**
+	 * Returns the relative path to the page that handles the display of the object
+	 * @return string
+	 */
+	public static function GetUIPage();
+	/**
+	 * Displays the details of the object
+	 */
+	public function DisplayDetails(WebPage $oPage, $bEditMode = false);
+}
+
+abstract class cmdbAbstractObject extends CMDBObject implements iDisplay
 {
 	protected $m_iFormId; // The ID of the form used to edit the object (when in edition mode !)
 	
@@ -48,40 +81,6 @@ abstract class cmdbAbstractObject extends CMDBObject
 		return '../pages/UI.php';
 	}
 	
-	public static function ComputeUIPage($sClass)
-	{
-		static $aUIPagesCache = array(); // Cache to store the php page used to display each class of object
-		if (!isset($aUIPagesCache[$sClass]))
-		{
-			$UIPage = false;
-			if (is_callable("$sClass::GetUIPage"))
-			{
-				$UIPage = eval("return $sClass::GetUIPage();"); // May return false in case of error
-			}
-			$aUIPagesCache[$sClass] = $UIPage === false ? './UI.php' : $UIPage;
-		}
-		$sPage = $aUIPagesCache[$sClass];
-		return $sPage;
-	}
-
-	protected static function MakeHyperLink($sObjClass, $sObjKey, $sLabel = '')
-	{
-		if ($sObjKey <= 0) return '<em>'.Dict::S('UI:UndefinedObject').'</em>'; // Objects built in memory have negative IDs
-
-		$oAppContext = new ApplicationContext();	
-		$sPage = self::ComputeUIPage($sObjClass);
-		$sAbsoluteUrl = utils::GetAbsoluteUrlPath();
-
-		// Safety net
-		//
-		if (empty($sLabel))
-		{
-			$sLabel = MetaModel::GetName($sObjClass)." #$sObjKey";
-		}
-		$sHint = MetaModel::GetName($sObjClass)."::$sObjKey";
-		return "<a href=\"{$sAbsoluteUrl}{$sPage}?operation=details&class=$sObjClass&id=$sObjKey&".$oAppContext->GetForLink()."\" title=\"$sHint\">$sLabel</a>";
-	}
-
 	function DisplayBareHeader(WebPage $oPage, $bEditMode = false)
 	{
 		// Standard Header with name, actions menu and history block

+ 45 - 3
core/dbobject.class.php

@@ -482,14 +482,56 @@ abstract class DBObject
 
 	protected static function MakeHyperLink($sObjClass, $sObjKey, $sLabel = '')
 	{
-		if ($sObjKey == 0) return '<em>undefined</em>';
+		if ($sObjKey <= 0) return '<em>'.Dict::S('UI:UndefinedObject').'</em>'; // Objects built in memory have negative IDs
 
-		return MetaModel::GetName($sObjClass)."::$sObjKey";
+		$oAppContext = new ApplicationContext();	
+		$sPage = self::ComputeUIPage($sObjClass);
+		$sAbsoluteUrl = utils::GetAbsoluteUrlPath();
+
+		// Safety net
+		//
+		if (empty($sLabel))
+		{
+			$sLabel = MetaModel::GetName($sObjClass)." #$sObjKey";
+		}
+		$sHint = MetaModel::GetName($sObjClass)."::$sObjKey";
+		return "<a href=\"{$sAbsoluteUrl}{$sPage}?operation=details&class=$sObjClass&id=$sObjKey&".$oAppContext->GetForLink()."\" title=\"$sHint\">$sLabel</a>";
 	}
 
 	public function GetHyperlink()
 	{
-		return $this->MakeHyperLink(get_class($this), $this->GetKey(), $this->GetName());
+		if ($this->IsNew()) return '<em>'.Dict::S('UI:UndefinedObject').'</em>'; // Objects built in memory have negative IDs
+
+		$oAppContext = new ApplicationContext();	
+		$sPage = $this->GetUIPage();
+		$sAbsoluteUrl = utils::GetAbsoluteUrlPath();
+		$sObjClass = get_class($this);
+		$sObjKey = $this->GetKey();
+
+		$sLabel = $this->GetName();
+		$sHint = MetaModel::GetName($sObjClass)."::$sObjKey";
+		return "<a href=\"{$sAbsoluteUrl}{$sPage}?operation=details&class=$sObjClass&id=$sObjKey&".$oAppContext->GetForLink()."\" title=\"$sHint\">$sLabel</a>";
+	}
+	
+	public static function ComputeUIPage($sClass)
+	{
+		static $aUIPagesCache = array(); // Cache to store the php page used to display each class of object
+		if (!isset($aUIPagesCache[$sClass]))
+		{
+			$UIPage = false;
+			if (is_callable("$sClass::GetUIPage"))
+			{
+				$UIPage = eval("return $sClass::GetUIPage();"); // May return false in case of error
+			}
+			$aUIPagesCache[$sClass] = $UIPage === false ? './UI.php' : $UIPage;
+		}
+		$sPage = $aUIPagesCache[$sClass];
+		return $sPage;
+	}
+
+	public static function GetUIPage()
+	{
+		return 'UI.php';
 	}
 
 

+ 6 - 1
dictionaries/dictionary.itop.core.php

@@ -539,7 +539,12 @@ Dict::Add('EN US', 'English', 'English', array(
 	'Core:Synchro:Nb_Class:Objects' => '%1$s: %2$s',
 	'Class:SynchroDataSource/Error:AtLeastOneReconciliationKeyMustBeSpecified' => 'At Least one reconciliation key must be specified.',			
 	'Class:SynchroDataSource/Error:DeleteRetentionDurationMustBeSpecified' => 'A delete retention period must be specified, since objects are to be deleted after being marked as obsolete',			
-	'Class:SynchroDataSource/Error:DeletePolicyUpdateMustBeSpecified' => 'Obsolete objects are to be updated, but no update is specified.',		
+	'Class:SynchroDataSource/Error:DeletePolicyUpdateMustBeSpecified' => 'Obsolete objects are to be updated, but no update is specified.',
+	'Core:SynchroReplica:PublicData' => 'Public Data',
+	'Core:SynchroReplica:PrivateDetails' => 'Private Details',
+	'Core:SynchroReplica:BackToDataSource' => 'Go Back to the Synchro Data Source: %1$s',
+	'Core:SynchroReplica:ListOfReplicas' => 'List of Replica',
+			
 ));
 
 //

+ 83 - 0
synchro/replica.php

@@ -0,0 +1,83 @@
+<?php
+// Copyright (C) 2011 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+/**
+ * Display and search synchro replicas
+ */
+require_once('../approot.inc.php');
+require_once(APPROOT.'/application/application.inc.php');
+require_once(APPROOT.'/application/itopwebpage.class.inc.php');
+
+require_once(APPROOT.'/application/startup.inc.php');
+
+require_once(APPROOT.'/application/loginwebpage.class.inc.php');
+LoginWebPage::DoLogin(true); // Check user rights and prompt if needed, admins only here !
+
+$sOperation = utils::ReadParam('operation', 'menu');
+$oAppContext = new ApplicationContext();
+
+$oP = new iTopWebPage("iTop - Synchro Replicas");
+
+// Main program
+$sOperation = utils::ReadParam('operation', 'details');
+try
+{
+	switch($sOperation)
+	{
+		case 'details':
+		$iId = utils::ReadParam('id', null);
+		if ($iId == null)
+		{
+			throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'id'));
+		}
+		$oReplica = MetaModel::GetObject('SynchroReplica', $iId);
+		$oReplica->DisplayDetails($oP);
+		break;
+		
+		case 'oql':
+		$sOQL = utils::ReadParam('oql', null);
+		if ($sOQL == null)
+		{
+			throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'oql'));
+		}
+		$oP->add('<p class="page-header">'.MetaModel::GetClassIcon('SynchroReplica').Dict::S('Core:SynchroReplica:ListOfReplicas').'</p>');
+		$iSourceId = utils::ReadParam('datasource', null);
+		if ($iSourceId != null)
+		{
+			$oSource = MetaModel::GetObject('SynchroDataSource', $iSourceId);
+			$oP->p(Dict::Format('Core:SynchroReplica:BackToDataSource', $oSource->GetHyperlink()).'</a>');
+		}
+		$oFilter = DBObjectSearch::FromOQL($sOQL);
+		$oBlock = new DisplayBlock($oFilter, 'list', false, array('menu'=>false));
+		$oBlock->Display($oP, 1);
+		break;
+	}
+}
+catch(CoreException $e)
+{
+	$oP->p('<b>An error occured while running the query:</b>');
+	$oP->p($e->getHtmlDesc());
+}
+catch(Exception $e)
+{
+	$oP->p('<b>An error occured while running the query:</b>');
+	$oP->p($e->getMessage());
+}
+
+$oP->output();
+?>
+
+?>

+ 2 - 1
synchro/synchro_exec.php

@@ -102,7 +102,6 @@ else
 
 	$oP = new WebPage(Dict::S("TitleSynchroExecution"));
 	$sDataSourcesList = utils::ReadParam('data_sources', null, true);
-	$bSimulate = (utils::ReadParam('simulate', '0', true) == '1');
 	
 	if ($sDataSourcesList == null)
 	{
@@ -110,6 +109,8 @@ else
 	}
 }
 
+$bSimulate = (utils::ReadParam('simulate', '0', true) == '1');
+
 
 foreach(explode(',', $sDataSourcesList) as $iSDS)
 {

+ 163 - 38
synchro/synchrodatasource.class.inc.php

@@ -174,6 +174,8 @@ class SynchroDataSource extends cmdbAbstractObject
 			$oLastLog = $oSetSynchroLog->Fetch();
 			$sStartDate = $oLastLog->Get('start_date');
 			$oLastLog->Get('stats_nb_replica_seen');
+			$iLastLog = 0;
+			$iDSid = $this->GetKey();
 			if ($oLastLog->Get('status') == 'running')
 			{
 				// Still running !
@@ -182,6 +184,7 @@ class SynchroDataSource extends cmdbAbstractObject
 			else
 			{
 				$sEndDate = $oLastLog->Get('end_date');
+				$iLastLog = $oLastLog->GetKey();
 				$oPage->p('<h2>'.Dict::Format('Core:Synchro:SynchroEndedOn_Date', $sEndDate).'</h2>');
 			}
 
@@ -204,6 +207,7 @@ class SynchroDataSource extends cmdbAbstractObject
 			$sScript .= "end: 'Done'";
 			$sScript .= "};\n";
 			$sScript .= <<<EOF
+			var sLastLog = '$iLastLog';
 	function UpdateSynoptics(id)
 	{
 		var aValues = aSynchroLog[id];
@@ -214,6 +218,33 @@ class SynchroDataSource extends cmdbAbstractObject
 			var fOpacity = (aValues[sKey] == 0) ? 0.3 : 1;
 			$('#'+sKey).fadeTo("slow", fOpacity);
 		}
+		//alert('id = '+id+', lastLog='+sLastLog+', id==sLastLog: '+(id==sLastLog)+' obj_updated_errors:  '+aValues['obj_updated_errors']);
+		if ( (id == sLastLog) && (aValues['obj_new_errors'] > 0) )
+		{
+			$('#new_errors_link').show();
+		}
+		else
+		{
+			$('#new_errors_link').hide();
+		}
+		
+		if ( (id == sLastLog) && (aValues['obj_updated_errors'] > 0) )
+		{
+			$('#updated_errors_link').show();
+		}
+		else
+		{
+			$('#updated_errors_link').hide();
+		}
+		
+		if ( (id == sLastLog) && (aValues['obj_disappeared_errors'] > 0) )
+		{
+			$('#disappeared_errors_link').show();
+		}
+		else
+		{
+			$('#disappeared_errors_link').hide();
+		}
 	}
 EOF
 ;
@@ -236,6 +267,7 @@ EOF
 	<tr>
 EOF
 );
+			$sBaseOQL = "SELECT SynchroReplica WHERE sync_source_id=".$this->GetKey()." AND status_last_error!=''";
 			$oPage->add($this->HtmlBox('repl_ignored', $aData, '#999').'<td colspan="2">&nbsp;</td>');
 			$oPage->add("</tr>\n<tr>");
 			$oPage->add($this->HtmlBox('repl_disappeared', $aData, '#630', 'rowspan="4"').'<td rowspan="4" class="arrow">=&gt;</td>'.$this->HtmlBox('obj_disappeared_no_action', $aData, '#333'));
@@ -244,13 +276,15 @@ EOF
 			$oPage->add("</tr>\n<tr>");
 			$oPage->add($this->HtmlBox('obj_obsoleted', $aData, '#630'));
 			$oPage->add("</tr>\n<tr>");
-			$oPage->add($this->HtmlBox('obj_disappeared_errors', $aData, '#C00'));
+			$sOQL = urlencode($sBaseOQL." AND status='obsolete'");
+			$oPage->add($this->HtmlBox('obj_disappeared_errors', $aData, '#C00', '', " <a style=\"color:#fff\" href=\"../synchro/replica?operation=oql&datasource=$iDSid&oql=$sOQL\" id=\"disappeared_errors_link\">Show</a>"));
 			$oPage->add("</tr>\n<tr>");
 			$oPage->add($this->HtmlBox('repl_existing', $aData, '#093', 'rowspan="3"').'<td rowspan="3" class="arrow">=&gt;</td>'.$this->HtmlBox('obj_unchanged', $aData, '#393'));
 			$oPage->add("</tr>\n<tr>");
 			$oPage->add($this->HtmlBox('obj_updated', $aData, '#3C3'));
 			$oPage->add("</tr>\n<tr>");
-			$oPage->add($this->HtmlBox('obj_updated_errors', $aData, '#C00'));
+			$sOQL = urlencode($sBaseOQL." AND status='modified'");
+			$oPage->add($this->HtmlBox('obj_updated_errors', $aData, '#C00', '', " <a style=\"color:#fff\" href=\"../synchro/replica?operation=oql&datasource=$iDSid&oql=$sOQL\" id=\"updated_errors_link\">Show</a>"));
 			$oPage->add("</tr>\n<tr>");
 			$oPage->add($this->HtmlBox('repl_new', $aData, '#339', 'rowspan="4"').'<td rowspan="4" class="arrow">=&gt;</td>'.$this->HtmlBox('obj_new_unchanged', $aData, '#393'));
 			$oPage->add("</tr>\n<tr>");
@@ -258,9 +292,11 @@ EOF
 			$oPage->add("</tr>\n<tr>");
 			$oPage->add($this->HtmlBox('obj_created', $aData, '#339'));
 			$oPage->add("</tr>\n<tr>");
-			$oPage->add($this->HtmlBox('obj_new_errors', $aData, '#C00'));
+			$sOQL = urlencode($sBaseOQL." AND status='new'");
+			$oPage->add($this->HtmlBox('obj_new_errors', $aData, '#C00', '', " <a style=\"color:#fff\" href=\"../synchro/replica?operation=oql&datasource=$iDSid&oql=$sOQL\" id=\"new_errors_link\">Show</a>"));
 			$oPage->add("</tr>\n</table>\n");
 			$oPage->add('</td></tr></table>');
+			$oPage->add_ready_script("UpdateSynoptics('$iLastLog')");
 		}
 		else
 		{
@@ -268,13 +304,14 @@ EOF
 		}
 	}
 	
-	protected function HtmlBox($sId, $aData, $sColor, $sHTMLAttribs = '')
+	protected function HtmlBox($sId, $aData, $sColor, $sHTMLAttribs = '', $sErrorLink = '')
 	{
 		$iCount = $aData[$sId];
 		$sCount = "<span id=\"c_{$sId}\">$iCount</span>";
 		$sLabel = Dict::Format('Core:Synchro:label_'.$sId, $sCount);
 		$sOpacity = ($iCount==0) ? "opacity:0.3;" : "";
-		return "<td id=\"$sId\" style=\"background-color:$sColor;$sOpacity;\" {$sHTMLAttribs}>$sLabel</td>";
+
+		return "<td id=\"$sId\" style=\"background-color:$sColor;$sOpacity;\" {$sHTMLAttribs}>{$sLabel}{$sErrorLink}</td>";
 	}
 	
 	protected function ProcessLog($oLastLog)
@@ -378,6 +415,37 @@ EOF
 	{
 		parent::DoCheckToWrite();
 
+		if ($this->IsNew())
+		{
+			// When inserting a new datasource object, also create the SynchroAttribute objects
+			// for each field of the target class
+			// Create all the SynchroAttribute records
+			$oAttributeSet = $this->Get('attribute_list');
+			foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef)
+			{
+				if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
+				{
+					$oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode);
+					if ($oAttDef->IsExternalKey())
+					{
+						$oAttribute = new SynchroAttExtKey();
+						$oAttribute->Set('reconciliation_attcode', ''); // Blank means by pkey
+					}
+					else
+					{
+						$oAttribute = new SynchroAttribute();
+					}
+					$oAttribute->Set('sync_source_id', $this->GetKey());
+					$oAttribute->Set('attcode', $sAttCode);
+					$oAttribute->Set('reconcile', MetaModel::IsReconcKey($this->GetTargetClass(), $sAttCode) ? 1 : 0);
+					$oAttribute->Set('update', 1);
+					$oAttribute->Set('update_policy', 'master_locked');
+					$oAttributeSet->AddObject($oAttribute);
+				}
+			}
+			$this->Set('attribute_list', $oAttributeSet);
+		}
+
 		// Check that there is at least one reconciliation key defined
 		if ($this->Get('reconciliation_policy') == 'use_attributes')
 		{
@@ -426,38 +494,6 @@ EOF
 	}
 
 	/**
-	 * When inserting a new datasource object, also create the SynchroAttribute objects
-	 * for each field of the target class
-	 */
-	protected function OnInsert()
-	{
-		// Create all the SynchroAttribute records
-		$oAttributeSet = $this->Get('attribute_list');
-		foreach(MetaModel::ListAttributeDefs($this->GetTargetClass()) as $sAttCode=>$oAttDef)
-		{
-			if ($oAttDef->IsScalar() && $oAttDef->IsWritable())
-			{
-				$oAttDef = MetaModel::GetAttributeDef($this->GetTargetClass(), $sAttCode);
-				if ($oAttDef->IsExternalKey())
-				{
-					$oAttribute = new SynchroAttExtKey();
-					$oAttribute->Set('reconciliation_attcode', ''); // Blank means by pkey
-				}
-				else
-				{
-					$oAttribute = new SynchroAttribute();
-				}
-				$oAttribute->Set('sync_source_id', $this->GetKey());
-				$oAttribute->Set('attcode', $sAttCode);
-				$oAttribute->Set('reconcile', MetaModel::IsReconcKey($this->GetTargetClass(), $sAttCode) ? 1 : 0);
-				$oAttribute->Set('update', 1);
-				$oAttribute->Set('update_policy', 'master_locked');
-				$oAttributeSet->AddObject($oAttribute);
-			}
-		}
-		$this->Set('attribute_list', $oAttributeSet);
-	}
-	/**
 	 * When the new datasource has been created, let's create the synchro_data table
 	 * that will hold the data records and the correspoding triggers which will maintain
 	 * both tables in sync
@@ -1084,7 +1120,7 @@ class SynchroLog extends DBObject
 }
 
 
-class SynchroReplica extends DBObject
+class SynchroReplica extends DBObject implements iDisplay
 {
 	static $aSearches = array(); // Cache of OQL queries used for reconciliation (per data source)
 	
@@ -1460,6 +1496,94 @@ class SynchroReplica extends DBObject
 
  		return $aData[$sColumnName];
 	 }
+	 
+	/**
+	 * Maps the given context parameter name to the appropriate filter/search code for this class
+	 * @param string $sContextParam Name of the context parameter, i.e. 'org_id'
+	 * @return string Filter code, i.e. 'customer_id'
+	 */
+	public static function MapContextParam($sContextParam)
+	{
+		if ($sContextParam == 'menu')
+		{
+			return null;
+		}
+		else
+		{
+			return $sContextParam;
+		}
+	}
+
+	/**
+	 * This function returns a 'hilight' CSS class, used to hilight a given row in a table
+	 * There are currently (i.e defined in the CSS) 4 possible values HILIGHT_CLASS_CRITICAL,
+	 * HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE
+	 * To Be overridden by derived classes
+	 * @param void
+	 * @return String The desired higlight class for the object/row
+	 */
+	public function GetHilightClass()
+	{
+		// Possible return values are:
+		// HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE	
+		return HILIGHT_CLASS_NONE; // Not hilighted by default
+	}
+
+	public static function GetUIPage()
+	{
+		return '../synchro/replica.php';
+	}
+
+	function DisplayDetails(WebPage $oPage, $bEditMode = false)
+	{
+		// Object's details
+		//$this->DisplayBareHeader($oPage, $bEditMode);
+		$oPage->AddTabContainer(OBJECT_PROPERTIES_TAB);
+		$oPage->SetCurrentTabContainer(OBJECT_PROPERTIES_TAB);
+		$oPage->SetCurrentTab(Dict::S('UI:PropertiesTab'));
+		$this->DisplayBareProperties($oPage, $bEditMode);
+	}
+	
+	function DisplayBareProperties(WebPage $oPage, $bEditMode = false)
+	{
+		if ($bEditMode) return; // Not editable
+		
+		$oPage->add('<table style="vertical-align:top"><tr style="vertical-align:top"><td>');
+		$aDetails = array();
+		$sClass = get_class($this);
+		$oPage->add('<fieldset>');
+		$oPage->add('<legend>'.Dict::S('Core:SynchroReplica:PrivateDetails').'</legend>');
+		$aZList = MetaModel::GetZListItems($sClass, 'details');
+		foreach( $aZList as $sAttCode)
+		{
+			$sDisplayValue = $this->GetAsHTML($sAttCode);	
+			$aDetails[] = array('label' => '<span title="'.MetaModel::GetDescription($sClass, $sAttCode).'">'.MetaModel::GetLabel($sClass, $sAttCode).'</span>', 'value' => $sDisplayValue);
+		}
+		$oPage->Details($aDetails);
+		$oPage->add('</fieldset>');
+		$oPage->add('</td><td>');
+		$oPage->add('<fieldset>');
+		$oPage->add('<legend>'.Dict::S('Core:SynchroReplica:PublicData').'</legend>');
+		$oSource = MetaModel::GetObject('SynchroDataSource', $this->Get('sync_source_id'));
+		
+		$sSQLTable = $oSource->GetDataTable();
+		$sSQL = "SELECT * FROM $sSQLTable WHERE id=".$this->GetKey();
+
+		$rQuery = CMDBSource::Query($sSQL);
+		$aData = CMDBSource::FetchArray($rQuery);
+
+		$aHeaders = array('attcode' => array('label' => 'Attribute Code', 'description' => ''),
+						  'data'    => array('label' => 'Value', 'description' => ''));
+		$aRows = array();
+		foreach($aData as $sKey => $value)
+		{
+			$aRows[] = array('attcode' => $sKey, 'data' => $value);
+		}
+		$oPage->Table($aHeaders, $aRows);
+		$oPage->add('</fieldset>');
+		$oPage->add('</td></tr></table>');
+		
+	}
 }
 
 // TO DO: finalize.... admins only ? which options ? troubleshoot WebPageMenuNode::__construct(.... sEnableClass...) ?
@@ -1467,6 +1591,7 @@ class SynchroReplica extends DBObject
 {
 	$oAdminMenu = new MenuGroup('AdminTools', 80 /* fRank */);
 	new OQLMenuNode('DataSources', 'SELECT SynchroDataSource', $oAdminMenu->GetIndex(), 12 /* fRank */, true, 'SynchroDataSource', UR_ACTION_MODIFY, UR_ALLOWED_YES);
+	new OQLMenuNode('Replicas', 'SELECT SynchroReplica', $oAdminMenu->GetIndex(), 12 /* fRank */, true, 'SynchroReplica', UR_ACTION_MODIFY, UR_ALLOWED_YES);
 	new WebPageMenuNode('Test:RunSynchro', '../synchro/synchro_exec.php', $oAdminMenu->GetIndex(), 13 /* fRank */, 'SynchroDataSource');
 }	
 ?>