View Issue Details

IDProjectCategoryView StatusLast Update
550RackTablesdefaultpublic2015-06-11 03:59
Reporteruser292Assigned Toinfrastation  
PrioritynormalSeverityfeatureReproducibilityN/A
Status assignedResolutionopen 
Summary550: tracking cable types
DescriptionHi,

I've discussed this idea with infrastation a while ago. My colleagues want to track which cable types are used for certain connections. I'm willing to write that code, but first I'd like to have a few opinions on my plan:

Changes in Database:
- Add a field `cable_type_id` UNSIGNED INTEGER to PortCompat which hold a ref to a chapter from which possible cable types for this kind of connection can be selected. ( This can be NULL if you don't want to use this feature for a certain connection )
- Add a field to `cable_type_key` UNSIGNED INTEGER to Link which points to a dictionary entry. ( This is NULL if the cable is not known. )

Changes in Interface:
- Extend the Port compatibility config page according to the database changes.
- Extend the Object -> Ports tab to show the cable type for connected ports.
- Extend the Link-to popup with a drop-down showing the available cable types.

Changes to functions:
- fetchPortList ( fetch the cable type )
- linkPorts ( optional additional argument )
- commitUpdatePortLink ( idem )
- handlePopupPortLink

Still to be considered:
- Track cable changes in PortLog?
- Interface if you just want to change the cable type but not the linked port.

Kind regards
Hannes
TagsNo tags attached.
Attached Files
track_cable_type.patch (31,600 bytes)   
diff -rupN '--exclude=.git' '--exclude=secret.php' racktables2/wwwroot/inc/database.php racktables/wwwroot/inc/database.php
--- racktables2/wwwroot/inc/database.php	2012-04-19 12:29:26.749825940 +0200
+++ racktables/wwwroot/inc/database.php	2012-04-19 12:20:06.809828661 +0200
@@ -728,6 +728,7 @@ SELECT
 	(SELECT PortInnerInterface.iif_name FROM PortInnerInterface WHERE PortInnerInterface.id = Port.iif_id) AS iif_name,
 	(SELECT Dictionary.dict_value FROM Dictionary WHERE Dictionary.dict_key = Port.type) AS oif_name,
 	IF(la.porta, la.cable, lb.cable) AS cableid,
+	IF(la.porta, la.cable_dict_key, lb.cable_dict_key) AS cable_dict_key,
 	IF(la.porta, pa.id, pb.id) AS remote_id,
 	IF(la.porta, pa.name, pb.name) AS remote_name,
 	IF(la.porta, pa.object_id, pb.object_id) AS remote_object_id,
@@ -1452,19 +1453,44 @@ function getAllIPv4Allocations ()
 	return $ret;
 }
 
-function linkPorts ($porta, $portb, $cable = NULL)
+function linkPorts ($porta, $portb, $cable = NULL, $cable_type = NULL, $update = false)
 {
 	if ($porta == $portb)
 		throw new InvalidArgException ('porta/portb', $porta, "Ports can't be the same");
 
 	global $dbxlink;
-	$dbxlink->exec ('LOCK TABLES Link WRITE');
+	# the many locks are necessary. otherwise commitUnlinkPort could not access these tables:
+	$dbxlink->exec ('LOCK TABLES Link WRITE, Port AS pa WRITE, Port AS pb WRITE , Object WRITE, RackObject WRITE, PortLog WRITE');
 	$result = usePreparedSelectBlade
 	(
-		'SELECT COUNT(*) FROM Link WHERE porta IN (?,?) OR portb IN (?,?)',
+		'SELECT * FROM Link WHERE porta IN (?,?) OR portb IN (?,?)',
 		array ($porta, $portb, $porta, $portb)
 	);
-	if ($result->fetchColumn () != 0)
+	if ( $update )
+	{
+		$a = $result->fetch();
+		if( $a ){
+			if( ($a['porta'] == $porta && $a['portb'] == $portb) || ($a['portb'] == $porta && $a['porta'] == $portb) )
+			{
+				// update!
+				$dbxlink->exec ('UNLOCK TABLES');
+				usePreparedUpdateBlade(
+					'Link',
+					array (
+						'cable' => ( mb_strlen ($cable) ? $cable : NULL ) ,
+						'cable_dict_key' => ( mb_strlen ($cable_type) ? intval($cable_type) : NULL )
+					),
+					array( 'porta' => $a['porta'], 'portb' => $a['portb'] ) 
+				);
+				
+				return ;
+			}else{
+				commitUnlinkPort($porta);
+				commitUnlinkPort($portb);
+			}
+		}
+	}
+	else if ($result->fetch ())
 	{
 		$dbxlink->exec ('UNLOCK TABLES');
 		return "Port ${porta} or ${portb} is already linked";
@@ -1483,7 +1509,8 @@ function linkPorts ($porta, $portb, $cab
 		(
 			'porta' => $porta,
 			'portb' => $portb,
-			'cable' => mb_strlen ($cable) ? $cable : NULL
+			'cable' => mb_strlen ($cable) ? $cable : NULL,
+			'cable_dict_key' => mb_strlen ($cable_type) ? intval($cable_type) : NULL
 		)
 	);
 	$dbxlink->exec ('UNLOCK TABLES');
@@ -2812,9 +2839,10 @@ function commitUpdateUserAccount ($id, $
 function getPortOIFCompat ()
 {
 	$query =
-		"select type1, type2, d1.dict_value as type1name, d2.dict_value as type2name from " .
+		"select type1, type2, d1.dict_value as type1name, d2.dict_value as type2name, cable_chapter_id , c.name as cable_chapter_name from " .
 		"PortCompat as pc inner join Dictionary as d1 on pc.type1 = d1.dict_key " .
 		"inner join Dictionary as d2 on pc.type2 = d2.dict_key " .
+		"left join Chapter as c on pc.cable_chapter_id = c.id ".
 		'ORDER BY type1name, type2name';
 	$result = usePreparedSelectBlade ($query);
 	return $result->fetchAll (PDO::FETCH_ASSOC);
diff -rupN '--exclude=.git' '--exclude=secret.php' racktables2/wwwroot/inc/interface.php racktables/wwwroot/inc/interface.php
--- racktables2/wwwroot/inc/interface.php	2012-04-16 09:51:40.040216558 +0200
+++ racktables/wwwroot/inc/interface.php	2012-04-19 12:20:06.813828661 +0200
@@ -1246,7 +1246,24 @@ function renderPortsForObject ($object_i
 					'object_id'=>$object_id)).
 			"'>";
 			printImageHREF ('cut', 'Unlink this port');
-			echo "</a></td>";
+			echo "</a>";
+			echo "<span";
+			$helper_args = array
+			(
+				'port' => $port['id'],
+			);
+			$popup_args = 'height=700, width=400, location=no, menubar=no, '.
+				'resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
+			echo " ondblclick='window.open(\"" . makeHrefForHelper ('portlist', $helper_args);
+			echo "\",\"findlink\",\"${popup_args}\");'";
+			// end of onclick=
+			echo " onclick='window.open(\"" . makeHrefForHelper ('portlist', $helper_args);
+			echo "\",\"findlink\",\"${popup_args}\");'";
+			// end of onclick=
+			echo '>';
+			// end of <a>
+			printImageHREF ('plug', 'Link this port');
+			echo "</span></td>";
 		}
 		elseif (strlen ($port['reservation_comment']))
 		{
@@ -3418,7 +3435,7 @@ function renderOIFCompatViewer()
 	$order = 'odd';
 	$last_left_oif_id = NULL;
 	echo '<br><table class=cooltable border=0 cellpadding=5 cellspacing=0 align=center>';
-	echo '<tr><th>From interface</th><th>To interface</th></tr>';
+	echo '<tr><th>From interface</th><th>To interface</th><th>Cable</th></tr>';
 	foreach (getPortOIFCompat() as $pair)
 	{
 		if ($last_left_oif_id != $pair['type1'])
@@ -3426,14 +3443,20 @@ function renderOIFCompatViewer()
 			$order = $nextorder[$order];
 			$last_left_oif_id = $pair['type1'];
 		}
-		echo "<tr class=row_${order}><td>${pair['type1name']}</td><td>${pair['type2name']}</td></tr>";
+		echo "<tr class=row_${order}><td>${pair['type1name']}</td><td>${pair['type2name']}</td><td>${pair['cable_chapter_name']}</td></tr>";
 	}
 	echo '</table>';
 }
 
 function renderOIFCompatEditor()
 {
-	function printNewitemTR()
+	$raw_chaplist = getChapterList();
+	$chaplist = array();
+	$chaplist[0]='';
+	foreach( $raw_chaplist as $chapter ){
+		$chaplist[$chapter['id']]= htmlspecialchars( $chapter['name'] );
+	}
+	function printNewitemTR($chaplist_local)
 	{
 		printOpFormIntro ('add');
 		echo '<tr><th class=tdleft>';
@@ -3442,6 +3465,10 @@ function renderOIFCompatEditor()
 		printSelect (readChapter (CHAP_PORTTYPE), array ('name' => 'type1'));
 		echo '</th><th class=tdleft>';
 		printSelect (readChapter (CHAP_PORTTYPE), array ('name' => 'type2'));
+		echo '</th><th class=tdleft>';
+		printSelect( $chaplist_local, array( 'name' => 'cable_chapter_id' ) );
+		echo '</th><th>';
+		printImageHREF ('add', 'add pair', TRUE);
 		echo '</th></tr></form>';
 	}
 
@@ -3466,9 +3493,9 @@ function renderOIFCompatEditor()
 	startPortlet ('interface by interface');
 	$last_left_oif_id = NULL;
 	echo '<br><table class=cooltable align=center border=0 cellpadding=5 cellspacing=0>';
-	echo '<tr><th>&nbsp;</th><th>From Interface</th><th>To Interface</th></tr>';
+	echo '<tr><th>&nbsp;</th><th>From Interface</th><th>To Interface</th><th>Cable</th><th></th></tr>';
 	if (getConfigVar ('ADDNEW_AT_TOP') == 'yes')
-		printNewitemTR();
+		printNewitemTR($chaplist);
 	foreach (getPortOIFCompat() as $pair)
 	{
 		if ($last_left_oif_id != $pair['type1'])
@@ -3476,13 +3503,19 @@ function renderOIFCompatEditor()
 			$order = $nextorder[$order];
 			$last_left_oif_id = $pair['type1'];
 		}
+		printOpFormIntro('upd',array('type1'=>$pair['type1'], 'type2' => $pair['type2']) );
 		echo "<tr class=row_${order}><td>";
 		echo '<a href="' . makeHrefProcess (array ('op' => 'del', 'type1' => $pair['type1'], 'type2' => $pair['type2'])) . '">';
 		printImageHREF ('delete', 'remove pair');
-		echo "</a></td><td class=tdleft>${pair['type1name']}</td><td class=tdleft>${pair['type2name']}</td></tr>";
+		echo "</a></td><td class=tdleft>${pair['type1name']}</td><td class=tdleft>${pair['type2name']}</td>";
+		echo '<td>';
+		printSelect($chaplist, array('name'=>'cable_chapter_id'), $pair['cable_chapter_id']);
+		echo '</td><td>';
+		printImageHREF ('save', 'Save changes', TRUE); 
+		echo "</td></tr></form>";
 	}
 	if (getConfigVar ('ADDNEW_AT_TOP') != 'yes')
-		printNewitemTR();
+		printNewitemTR($chaplist);
 	echo '</table>';
 	finishPortlet();
 }
diff -rupN '--exclude=.git' '--exclude=secret.php' racktables2/wwwroot/inc/navigation.php racktables/wwwroot/inc/navigation.php
--- racktables2/wwwroot/inc/navigation.php	2012-04-16 09:51:40.044216558 +0200
+++ racktables/wwwroot/inc/navigation.php	2012-04-19 12:20:06.813828661 +0200
@@ -435,6 +435,7 @@ $tabhandler['portmap']['default'] = 'ren
 $tabhandler['portmap']['edit'] = 'renderOIFCompatEditor';
 $ophandler['portmap']['edit']['add'] = 'tableHandler';
 $ophandler['portmap']['edit']['del'] = 'tableHandler';
+$ophandler['portmap']['edit']['upd'] = 'tableHandler';
 $ophandler['portmap']['edit']['addPack'] = 'addOIFCompatPack';
 $ophandler['portmap']['edit']['delPack'] = 'delOIFCompatPack';
 
diff -rupN '--exclude=.git' '--exclude=secret.php' racktables2/wwwroot/inc/ophandlers.php racktables/wwwroot/inc/ophandlers.php
--- racktables2/wwwroot/inc/ophandlers.php	2012-04-19 12:29:26.757825940 +0200
+++ racktables/wwwroot/inc/ophandlers.php	2012-04-19 12:20:06.813828661 +0200
@@ -262,6 +262,21 @@ $opspec_list['portmap-edit-add'] = array
 	(
 		array ('url_argname' => 'type1', 'assertion' => 'uint'),
 		array ('url_argname' => 'type2', 'assertion' => 'uint'),
+		array ('url_argname' => 'cable_chapter_id', 'assertion' => 'uint0', 'if_empty' => 'NULL' )
+	),
+);
+$opspec_list['portmap-edit-upd'] = array
+(
+	'table' => 'PortCompat',
+	'action' => 'UPDATE',
+	'set_arglist' => array
+	(
+		array ('url_argname' => 'cable_chapter_id', 'assertion' => 'uint0', 'if_empty' => 'NULL'),
+	),
+	'where_arglist' => array
+	(
+		array ('url_argname' => 'type1', 'assertion' => 'uint'),
+		array ('url_argname' => 'type2', 'assertion' => 'uint')
 	),
 );
 $opspec_list['portmap-edit-del'] = array
diff -rupN '--exclude=.git' '--exclude=secret.php' racktables2/wwwroot/inc/popup.php racktables/wwwroot/inc/popup.php
--- racktables2/wwwroot/inc/popup.php	2012-04-16 09:51:40.044216558 +0200
+++ racktables/wwwroot/inc/popup.php	2012-04-19 12:39:08.649823113 +0200
@@ -34,7 +34,7 @@ function getProximateRacks ($rack_id, $p
 	return $ret;
 }
 
-function findSparePorts ($port_info, $filter)
+function getSparePorts($port_info, $filter)
 {
 	$qparams = array ();
 	$query = "
@@ -45,37 +45,34 @@ SELECT
 	p.iif_id,
 	p.type as oif_id,
 	pii.iif_name,
-	d.dict_value as oif_name,
 	p.object_id,
 	o.name as object_name
 FROM Port p
 INNER JOIN Object o ON o.id = p.object_id
 INNER JOIN PortInnerInterface pii ON p.iif_id = pii.id
-INNER JOIN Dictionary d ON d.dict_key = p.type
 ";
 	// porttype filter (non-strict match)
 	$query .= "
 INNER JOIN (
-	SELECT Port.id FROM Port
+	SELECT p2.id FROM Port p2
 	INNER JOIN
 	(
-		SELECT DISTINCT	pic2.iif_id
+		SELECT DISTINCT	pic2.iif_id, pic2.oif_id
 		FROM PortInterfaceCompat pic2
 		INNER JOIN PortCompat pc ON pc.type2 = pic2.oif_id
 ";
 		if ($port_info['iif_id'] != 1)
 		{
-			$query .= " INNER JOIN PortInterfaceCompat pic ON pic.oif_id = pc.type1 WHERE pic.iif_id = ? AND ";
+			$query .= " INNER JOIN PortInterfaceCompat pic ON pic.oif_id = pc.type1 WHERE pic.iif_id = ? ";
 			$qparams[] = $port_info['iif_id'];
 		}
 		else
 		{
-			$query .= " WHERE pc.type1 = ? AND ";
+			$query .= " WHERE pc.type1 = ? ";
 			$qparams[] = $port_info['oif_id'];
 		}
 		$query .= "
-			pic2.iif_id <> 1
-	) AS sub1 USING (iif_id)
+	) AS sub1 ON p2.iif_id = sub1.iif_id AND ( p2.iif_id <> 1 OR p2.type = sub1.oif_id )
 	UNION
 	SELECT Port.id
 	FROM Port
@@ -113,42 +110,104 @@ INNER JOIN (
 	// ordering
 	$query .= ' ORDER BY o.name';
 
-	$ret = array();
-	$result = usePreparedSelectBlade ($query, $qparams);
-	
-	$rows_by_pn = array();
-	$prev_object_id = NULL;
+	return usePreparedSelectBlade ($query, $qparams);
+}
+
+function getLinkPortJavascript($port_info, $ports)
+{
+	$required_oif = array();
+	$required_iif = array();
+	$required_chapter = array();
+	if( count($ports) == 0 ){
+		return '';
+	}
+	$jsports = array();
+	$compat = array();
+	$buffer = array('$(function($){');
+	$buffer[]='var PORT = { id:'.$port_info['id'].', name:"'.addslashes($port_info['name']).'",';
+	$buffer[]='	object_name:"'.addslashes($port_info['object_name']).'",';
+	$buffer[]='	oif_id:'.($port_info['oif_id'] ? $port_info['oif_id'] : 'null').', ';
+	$buffer[]='	iif_id:'.$port_info['iif_id'].', ';
+	$buffer[]='	cable:"'.addslashes($port_info['cableid']).'", ';
+	$buffer[]='	cable_dict_key:'.($port_info['cable_dict_key'] ? $port_info['cable_dict_key'] : 'null' ).', ';
+	$buffer[]='	remote_id:'.($port_info['remote_id'] ? $port_info['remote_id'] : 'null').'};';
+	$buffer[]='var PORTS = [';
 	
-	// fetch port rows from the DB
-	while (TRUE)
+	$required_iif[$port_info['iif_id']]=true;	
+	if( $port_info['oif_id'] ) $required_oif[$port_info['oif_id']]=true;
+	foreach( $ports as $port ){
+		$required_iif[ $port['iif_id'] ]=true;
+		if( $port['oif_id'] ) $required_oif[$port['oif_id']]=true;
+		$buffer[]='  { id:'.$port['id'].', name:"'.addslashes($port['name']).'", object_name:"'.addslashes($port['object_name']).'", oif_id:'.($port['oif_id'] ? $port['oif_id'] : 'null').', iif_id:'.$port['iif_id'].', reservation:"'.addslashes($port['reservation_comment']).'"},';
+	}
+	$buffer[]='];';
+
+	$incom = usePreparedSelectBlade('SELECT * FROM PortInterfaceCompat WHERE iif_id IN ('.questionMarks(count($required_iif)).') ORDER BY iif_id ', array_keys($required_iif));
+	$buffer[]='var INNER_OUTER_COMPATIBILITY = {';
+	$last_iif_id = null;
+	foreach( $incom as $in )
 	{
-		$row = $result->fetch (PDO::FETCH_ASSOC);
-		if (isset ($prev_object_id) and (! $row or $row['object_id'] != $prev_object_id))
+		if( $in['iif_id'] != $last_iif_id )
 		{
-			// handle sorted object's portlist
-			foreach (sortPortList ($rows_by_pn) as $ports_subarray)
-				foreach ($ports_subarray as $port_row)
-				{
-					$port_description = $port_row['object_name'] . ' --  ' . $port_row['name'];
-					if (count ($ports_subarray) > 1)
-					{
-						$if_type = $port_row['iif_id'] == 1 ? $port_row['oif_name'] : $port_row['iif_name'];
-						$port_description .= " ($if_type)";
-					}
-					if (! empty ($port_row['reservation_comment']))
-						$port_description .= '  --  ' . $port_row['reservation_comment'];
-					$ret[$port_row['id']] = $port_description;
-				}
-			$rows_by_pn = array();
+			if( $last_iif_id != null )
+			{
+				$buffer[]='  ],';
+			}
+			$buffer[]='  '.$in['iif_id'].': [';
+			$last_iif_id = $in['iif_id'];
 		}
-		$prev_object_id = $row['object_id'];
-		if ($row)
-			$rows_by_pn[$row['name']][] = $row;
-		else
-			break;
+		$required_oif[$in['oif_id']]=true;
+		$buffer[]='    '.$in['oif_id'].',';
 	}
+	if( $last_iif_id != null )
+	{
+		$buffer[]='  ]';
+	}
+	$buffer[]='};';
 
-	return $ret;
+	$query ='SELECT * FROM PortCompat pc'.
+		' WHERE type1 IN ('.questionMarks(count($required_oif)).') AND type2 IN ('.questionMarks(count($required_oif)).');';
+	$types = usePreparedSelectBlade($query, array_merge( array_keys($required_oif), array_keys($required_oif) ) );
+	foreach( $types as $type ){
+		@$compat[ $type['type1'] ][ $type['type2'] ] = array( 'cable_chapter_id'=> ($type['cable_chapter_id'] ? intval($type['cable_chapter_id']) : 'null') );
+		if( $type['cable_chapter_id'] ) $required_chapter[intval($type['cable_chapter_id'])] = true;
+	}
+	$buffer[]='var OUTER_COMPATIBILITY = {';
+	foreach( $compat as $a => $sub ){
+		$buffer[]='  '.$a.': { ';
+		foreach( $sub as $b => $info ){
+			$buffer[]='    '.$b.': {cable_chapter_id:'.$info['cable_chapter_id'].'},';
+		}
+		$buffer[]='  },';
+	}
+	$buffer[]='};';
+	if(	$required_chapter ){
+		$buffer[]='var CABLES = {';
+		$last_chapter_id = -1;
+		foreach( array_keys($required_chapter) as $chapter_id ){
+			$chapter = cookOptgroups( readChapter( $chapter_id, 'o' ) );
+			$buffer[]= $chapter_id.': [';
+			foreach( $chapter as $group => $entries ){
+				$buffer[]= '  {name: "'.addslashes($group).'", values:[';
+				foreach( $entries as $key => $value ){
+					$buffer[]= '    {key: '.$key.', value: "'.addslashes($value).'"},';
+				}
+				$buffer[]='  ]},';
+			}
+			$buffer[]='],';
+		}
+		$buffer[]='};';
+	}else{
+		$buffer[]='var CABLES = {};';
+	}
+	$oifs = usePreparedSelectBlade('SELECT dict_key,dict_value FROM Dictionary WHERE chapter_id = 2 AND dict_key IN ('.questionMarks(count($required_oif)).');', array_keys($required_oif));
+	$buffer[]='var OIF = {};';
+	foreach( $oifs as $oif ){
+		$buffer[]='OIF['.$oif['dict_key'].']= "'.addslashes($oif['dict_value']).'";';
+	}
+	$buffer[]='  initializeLinkportForm( PORT, PORTS, INNER_OUTER_COMPATIBILITY, OUTER_COMPATIBILITY, CABLES, OIF )';
+	$buffer[]='});';
+	return join("\n",$buffer);
 }
 
 // Return a list of all objects which are possible parents
@@ -221,131 +280,66 @@ function renderPopupObjectSelector()
 
 function handlePopupPortLink()
 {
+	global $dbxlink;
 	assertUIntArg ('port');
 	assertUIntArg ('remote_port');
 	assertStringArg ('cable', TRUE);
 	$port_info = getPortInfo ($_REQUEST['port']);
 	$remote_port_info = getPortInfo ($_REQUEST['remote_port']);
-	$POIFC = getPortOIFCompat();
-	if (isset ($_REQUEST['port_type']) and isset ($_REQUEST['remote_port_type']))
-	{
-		$type_local = $_REQUEST['port_type'];
-		$type_remote = $_REQUEST['remote_port_type'];
-	}
-	else
-	{
-		$type_local = $port_info['oif_id'];
-		$type_remote = $remote_port_info['oif_id'];
-	}
-	$matches = FALSE;
-	$js_table = '';
-	foreach ($POIFC as $pair)
-		if ($pair['type1'] == $type_local && $pair['type2'] == $type_remote)
-		{
-			$matches = TRUE;
-			break;
+	$dbxlink->beginTransaction();
+	try{
+		
+		if( $port_info['iif_id'] != 1 ){
+			// updateable
+			assertUIntArg ('local_oif');
+			if( $_POST['local_oif'] != $port_info['oif_id'] ){
+				commitUpdatePortOIF( $port_info['id'], $_POST['local_oif']);
+				$port_info['oif_id'] = intval($_POST['local_oif']);
+			}
 		}
-		else
-			$js_table .= "POIFC['${pair['type1']}-${pair['type2']}'] = 1;\n";
-
-	if ($matches)
-	{
-		if ($port_info['oif_id'] != $type_local)
-			commitUpdatePortOIF ($port_info['id'], $type_local);
-		if ($remote_port_info['oif_id'] != $type_remote)
-			commitUpdatePortOIF ($remote_port_info['id'], $type_remote);
-		linkPorts ($port_info['id'], $remote_port_info['id'], $_REQUEST['cable']);
-		showOneLiner 
-		(
-			8, 
-			array
+		if( $remote_port_info['iif_id'] != 1 ){
+			// updateable
+			assertUIntArg ('remote_oif');
+			if( $_POST['remote_oif'] != $remote_port_info['oif_id'] ){
+				commitUpdatePortOIF( $remote_port_info['id'], $_POST['remote_oif']);
+				$remote_port_info['oif_id'] = intval($_POST['remote_oif']);
+			}
+		}
+		// now check compat
+		$compat = usePreparedSelectBlade('SELECT * FROM PortCompat WHERE ( type1 = ? AND type2 = ? ) OR ( type2 = ? AND type1= ? ) ',
+			array( $port_info['oif_id'],$remote_port_info['oif_id'],$port_info['oif_id'],$remote_port_info['oif_id']) )->fetchAll();
+		if( !$compat ){
+			throw new InvalidRequestArgException('oif_id', '', 'Ports are not compatible');	
+		}else{
+			// :)
+			$cable_type = null;
+			if( $compat[0]['cable_chapter_id'] && $_POST['cable_type'] ){
+				assertUIntArg('cable_type',true);
+				$cable_type = $_POST['cable_type'];
+			}
+			linkPorts( $port_info['id'], $remote_port_info['id'], $_POST['cable'], $cable_type, true );
+			$dbxlink->commit();
+			showOneLiner 
 			(
-				formatPortLink ($port_info['id'], $port_info['name'], NULL, NULL),
-				formatPort ($remote_port_info),
-			)
-		);
-		addJS (<<<END
-window.opener.location.reload(true);
-window.close();
+				8, 
+				array
+				(
+					formatPortLink ($port_info['id'], $port_info['name'], NULL, NULL),
+					formatPort ($remote_port_info),
+				)
+			);
+			addJS (<<<END
+	window.opener.location.reload(true);
+	window.close();
 END
-		, TRUE);
-	}
-	else
-	{
-		// JS code to display port compatibility hint
-		addJS (<<<END
-POIFC = {};
-$js_table
-$(document).ready(function () {
-	$('select.porttype').change(onPortTypeChange);	
-	onPortTypeChange();
-});
-function onPortTypeChange() {
-	var key = $('*[name=port_type]')[0].value + '-' + $('*[name=remote_port_type]')[0].value;
-	if (POIFC[key] == 1)
-	{
-		$('#hint-not-compat').hide();
-		$('#hint-compat').show();
-	}
-	else
-	{
-		$('#hint-compat').hide();
-		$('#hint-not-compat').show();
-	}
-}
-END
-		, TRUE);
-		addCSS (<<<END
-.compat-hint {
-	display: none;
-	font-size: 125%;
-}
-.compat-hint#hint-compat {
-	color: green;
-}
-.compat-hint#hint-not-compat {
-	color: #804040;
-}
-END
-		, TRUE);
-		// render port type editor form
-		echo '<form method=GET>';
-		echo '<input type=hidden name="module" value="popup">';
-		echo '<input type=hidden name="helper" value="portlist">';
-		echo '<input type=hidden name="port" value="' . $port_info['id'] . '">';
-		echo '<input type=hidden name="remote_port" value="' . $remote_port_info['id'] . '">';
-		echo '<input type=hidden name="cable" value="' . htmlspecialchars ($_REQUEST['cable'], ENT_QUOTES) . '">';
-		echo '<p>The ports you have selected are not compatible. Please select a compatible transceiver pair.';
-		echo '<p>';
-		echo formatPort ($port_info) . ' ';
-		if ($port_info['iif_id'] == 1)
-		{
-			echo formatPortIIFOIF ($port_info);
-			echo '<input type=hidden name="port_type" value="' . $port_info['oif_id'] . '">';
-		}
-		else
-		{
-			echo '<label>' . $port_info['iif_name'] . ' ';
-			printSelect (getExistingPortTypeOptions ($port_info['id']), array ('class' => 'porttype', 'name' => 'port_type'), $type_local);
-			echo '</label>';
+			, true);
+			return;
 		}
-		echo ' &mdash; ';
-		if ($remote_port_info['iif_id'] == 1)
-		{
-			echo formatPortIIFOIF ($remote_port_info);
-			echo '<input type=hidden name="remote_port_type" value="' . $remote_port_info['oif_id'] . '">';
-		}
-		else
-		{
-			echo '<label>' . $remote_port_info['iif_name'] . ' ';
-			printSelect (getExistingPortTypeOptions ($remote_port_info['id']), array ('class' => 'porttype', 'name' => 'remote_port_type'), $type_remote);
-			echo '</label>';
-		}
-		echo ' ' . formatPort ($remote_port_info);
-		echo '<p class="compat-hint" id="hint-not-compat">&#10005; Not compatible port types</p>';
-		echo '<p class="compat-hint" id="hint-compat">&#10004; Compatible port types</p>';
-		echo '<p><input type=submit name="do_link" value="Link">';
+	}catch( Exception $e ){
+		$dbxlink->rollBack();
+		throw $e;
 	}
+	return renderPopupPortSelector();
 }
 
 function renderPopupPortSelector()
@@ -353,6 +347,7 @@ function renderPopupPortSelector()
 	assertUIntArg ('port');
 	$port_id = $_REQUEST['port'];
 	$port_info = getPortInfo ($port_id);
+
 	$in_rack = isset ($_REQUEST['in_rack']);
 
 	// fill port filter structure
@@ -379,11 +374,27 @@ function renderPopupPortSelector()
 		! empty ($filter['objects']) ||
 		! empty ($filter['ports'])
 	)
-		$spare_ports = findSparePorts ($port_info, $filter);
-
+	$ports_with_current = $ports = getSparePorts($port_info, $filter)->fetchAll();
+	if( $port_info['remote_id'] ){
+		$current = $ports_with_current[]= getPortInfo($port_info['remote_id']);
+	}
+	addJS('js/link_port_form.js');
+	addJS(getLinkPortJavascript($port_info, $ports_with_current),true);
+	addCSS (<<<'END'
+.reserved {
+	text-decoration: line-through;
+}
+.compatible {
+	background: #afa;
+}
+.incompatible {
+	background: #faa;
+}
+END
+		, TRUE);
 	// display search form
 	echo 'Link ' . formatPort ($port_info) . ' to...';
-	echo '<form method=GET>';
+	echo '<form method="POST">';
 	startPortlet ('Port list filter');
 	echo '<input type=hidden name="module" value="popup">';
 	echo '<input type=hidden name="helper" value="portlist">';
@@ -391,23 +402,59 @@ function renderPopupPortSelector()
 	echo '<table align="center" valign="bottom"><tr>';
 	echo '<td class="tdleft"><label>Object name:<br><input type=text size=8 name="filter-obj" value="' . htmlspecialchars ($filter['objects'], ENT_QUOTES) . '"></label></td>';
 	echo '<td class="tdleft"><label>Port name:<br><input type=text size=6 name="filter-port" value="' . htmlspecialchars ($filter['ports'], ENT_QUOTES) . '"></label></td>';
+	echo '</tr><tr>';
 	echo '<td class="tdleft" valign="bottom"><label><input type=checkbox name="in_rack"' . ($in_rack ? ' checked' : '') . '>Nearest racks</label></td>';
 	echo '<td valign="bottom"><input type=submit value="show ports"></td>';
 	echo '</tr></table>';
 	finishPortlet();
 
 	// display results
-	startPortlet ('Compatible spare ports');
-	if (empty ($spare_ports))
-		echo '(nothing found)';
-	else
-	{
-		echo getSelect ($spare_ports, array ('name' => 'remote_port', 'size' => getConfigVar ('MAXSELSIZE')), NULL, FALSE);
-		echo "<p>Cable ID: <input type=text id=cable name=cable>";
-		echo "<p><input type='submit' value='Link' name='do_link'>";
+	startPortlet ('Link port');
+	if( $ports_with_current ){
+		echo '<div style="float: left;width:40%">';
+		echo '<select name="remote_port" id="remote_port" size="',getConfigVar ('MAXSELSIZE'),'" style="width:100%">';
+		if( $current ){
+			echo '<optgroup label="current">';
+			echo '<optgroup label="&nbsp; ', htmlspecialchars($current['object_name']), '">';
+			echo '<option value="',$current['id'],'" selected="selected">', htmlspecialchars($current['name'] ),'</option>';
+			echo '</optgroup>';
+		}
+		echo '<optgroup label="search result">';
+		if( !$ports ){
+			echo '<option disabled="disabled" value="">nothing found</option>';
+		}else{
+			$ports_by_object = array();
+			foreach( $ports as $port ){
+				if( !isset($ports_by_object[$port['object_name']]) ) $ports_by_object[$port['object_name']] = array();
+				$ports_by_object[$port['object_name']][] = $port;
+			}
+			ksort( $ports_by_object );
+			foreach( $ports_by_object as $name => $object_ports ){
+				echo '<optgroup label="&nbsp; ', htmlspecialchars($name), '">';
+				foreach( sortPortList($object_ports, true) as $port ){
+					echo '<option value="', $port['id'],'"',($port['reservation_comment'] ? ' class="reserved"':''),'>', htmlspecialchars($port['name']), '</option>';
+				}
+				echo '</optgroup>';
+			}
+		}
+		echo '</optgroup>';
+		echo '</select>';
+		echo '</div>';
+		echo '<div style="margin-left: 41%">';
+		echo '<p style="display:none">This port is reserved: <span id="reservation"></span></p>';
+		echo '<p style="display:none"><label for="cable_type">Local Transceiver</label></br><select name="local_oif" id="local_oif" style="width:100%"></select></p>';
+		echo '<p style="display:none"><label for="remote_oif">Remote Transceiver</label></br><select name="remote_oif" id="remote_oif" style="width:100%"></select></p>';
+		echo '<p style="display:none"><label for="cable_type">Cable type</label></br><select name="cable_type" id="cable_type" style="width:100%"></select></p>';
+		echo '<p><label for="cable">Cable ID: </label><br /><input type="text" id="cable" name="cable" style="width:100%" value="',htmlspecialchars($port_info['cableid']) ,'"/></p>';
+		echo '<p><input type="submit" value="Link" name="do_link" id="submit"></p>';
+		echo '<br style="clear:both" /></div>';
+	}else{
+		echo 'nothing found';
 	}
+
 	finishPortlet();
 	echo '</form>';
+	echo '<br style="clear:both" />';
 }
 
 function renderPopupIPv4Selector()
diff -rupN '--exclude=.git' '--exclude=secret.php' racktables2/wwwroot/js/link_port_form.js racktables/wwwroot/js/link_port_form.js
--- racktables2/wwwroot/js/link_port_form.js	1970-01-01 01:00:00.000000000 +0100
+++ racktables/wwwroot/js/link_port_form.js	2012-04-19 12:20:06.813828661 +0200
@@ -0,0 +1,147 @@
+function initializeLinkportForm( PORT, PORTS, INNER_OUTER_COMPATIBILITY, OUTER_COMPATIBILITY, CABLES, OIF ){
+	var $remote_port = $('#remote_port'),$remote_oif = $('#remote_oif'),$local_oif = $('#local_oif'),$cable_type = $('#cable_type'), $submit = $('#submit'), $reservation = $('#reservation');
+	var compatClass = function(a,b){
+		if( !a || !b ){
+			return '';
+		}else if( !OUTER_COMPATIBILITY[a] || !OUTER_COMPATIBILITY[a][b]  ){
+			return 'incompatible';
+		}else{
+			return 'compatible';
+		}
+	}
+	var updateSelects = function(){
+		var rp = $remote_port.val(), ro = $remote_oif.val(), lo = $local_oif.val(), ct = $cable_type.val();
+		if( ro ) ro = parseInt(ro);
+		if( !lo ){
+			lo = PORT.oif_id;
+		}else{
+			lo = parseInt(lo);
+		}
+		if( !rp ){
+			rp = PORT.remote_id;
+		}
+		if( !ct ){
+			ct = PORT.cable_dict_key;
+		}
+		//remote_oif
+		$remote_oif.empty();
+		var remote_port;
+		if( rp ){
+			remote_port =	$.grep(PORTS,function(p){ return p.id == rp })[0];
+			if( !ro || remote_port.iif_id == 1 || INNER_OUTER_COMPATIBILITY[ remote_port.iif_id ].indexOf(ro) == -1  ){
+				ro = remote_port.oif_id;
+			}
+			if( remote_port.reservation == "" ){
+				$reservation.closest('p').hide();
+			}else{
+				$reservation.text( remote_port.reservation );
+				$reservation.closest('p').show();	
+			}
+			if( remote_port.iif_id != 1 ){
+				$remote_oif.closest('p').show();	
+				$remote_oif.attr('disabled',false);
+			}else{
+				$remote_oif.closest('p').hide();
+				$remote_oif.attr('disabled',true);
+			}
+			var rps = INNER_OUTER_COMPATIBILITY[ remote_port.iif_id ];
+			rps.sort(function(a,b){
+				return OIF[a].localeCompare(OIF[b]);
+			}).forEach( function(oif){
+				$('<option>').val(oif).text( OIF[oif] ).addClass( compatClass( oif, lo )  ).appendTo($remote_oif);
+			});
+			$remote_oif.val(ro);
+		}else{
+			$remote_oif.closest('p').hide();	
+			$remote_oif.attr('disabled',true);
+		}
+		//local_oif
+		$local_oif.empty();
+		if( PORT.iif_id != 1 ){
+			$local_oif.closest('p').show();
+			$local_oif.attr('disabled',false);
+		}else{
+			$local_oif.closest('p').hide();
+			$local_oif.attr('disabled',true);	
+		}
+		var lps = INNER_OUTER_COMPATIBILITY[ PORT.iif_id ];
+		lps.sort(function(a,b){
+			return OIF[a].localeCompare(OIF[b]);
+		}).forEach( function(oif){
+			$('<option>').val(oif).text( OIF[oif] ).addClass( compatClass( oif, ro ) ).appendTo($local_oif);
+		});
+		$local_oif.val(lo);
+		if( rp && lo && ro ){
+			// cables and compat!
+			var comb = OUTER_COMPATIBILITY[ ro ] ? OUTER_COMPATIBILITY[ ro ][ lo ] : null;
+			if( !comb ){
+				$submit.attr('disabled',true);
+				$submit.val('Incompatible transceiver');
+				$cable_type.closest('p').hide();
+			}else{
+				if( comb.cable_chapter_id ){
+					$cable_type.empty();
+					var cables = CABLES[comb.cable_chapter_id];
+					if( cables ){
+						var other;
+						$.each(cables,function(i,group){
+							if( group.name == 'other' ){
+								other = group;
+								return ;
+							}
+							var g = $('<optgroup>').attr('label',group.name).appendTo($cable_type);
+							$.each(group.values, function(j, opt){
+								$('<option>').val(opt.key).text(opt.value).appendTo(g);
+							});
+						});
+						if( other ){
+							$.each(other.values, function(j, opt){
+								$('<option>').val(opt.key).text(opt.value).appendTo($cable_type);
+							});
+						}
+					}
+				 $('<option>').val('').text('').appendTo($cable_type);	
+					$cable_type.val(ct);
+					$cable_type.closest('p').show();
+				}else{
+					// compatible but no cables
+					$cable_type.closest('p').hide();
+				}
+				$submit.attr('disabled',false);
+				$submit.val('Link');
+			}
+		}else{
+			if( !rp ){
+				$submit.attr('disabled',true);
+				$submit.val('Select a port');	
+			}
+			$cable_type.closest('p').hide();
+		}
+	}
+	updateSelects();
+	$remote_port.change(updateSelects);
+	var changeTransceiver = function( $a, $b ){
+		var av = $a.val();
+		var bv = $b.val();
+		if( !OUTER_COMPATIBILITY[av] ){
+			return ;
+		}
+		if(	OUTER_COMPATIBILITY[av][bv] ){
+			return;
+		}
+		$b.children('option').each(function(i,o ){
+			if( OUTER_COMPATIBILITY[ av ][ $(o).val() ] ){
+				$b.val( $(o).val() );
+				return false;
+			}
+		});
+	}
+	$local_oif.change(function(){
+		changeTransceiver($local_oif,$remote_oif);
+		updateSelects();
+	});
+	$remote_oif.change(function(){
+		changeTransceiver($remote_oif,$local_oif);
+		updateSelects();
+	}); 
+}
track_cable_type.patch (31,600 bytes)   
track_cable_type_0.19.13.diff (31,959 bytes)   
diff --git a/wwwroot/inc/database.php b/wwwroot/inc/database.php
index d9f8e2c..1b9dd82 100644
--- a/wwwroot/inc/database.php
+++ b/wwwroot/inc/database.php
@@ -580,9 +580,10 @@ function getObjectPortsAndLinks ($object_id)
 	{
 		$portid = $ret[$tmpkey]['id'];
 		$remote_id = NULL;
-		$query = "select porta, portb, cable from Link where porta = ? or portb = ?";
+		$query = "select porta, portb, cable, cable_dict_key from Link where porta = ? or portb = ?";
 		$result = usePreparedSelectBlade ($query, array ($portid, $portid));
 		$cable = "CableID n/a";
+		$cable_dict_key = null;
 		if ($row = $result->fetch (PDO::FETCH_ASSOC))
 		{
 			if ($portid != $row['porta'])
@@ -590,6 +591,7 @@ function getObjectPortsAndLinks ($object_id)
 			elseif ($portid != $row['portb'])
 				$remote_id = $row['portb'];
 			$cable = $row['cable'];
+			$cable_dict_key = $row['cable_dict_key'];
 		}
 		unset ($result);
 		if ($remote_id) // there's a remote end here
@@ -601,6 +603,7 @@ function getObjectPortsAndLinks ($object_id)
 				$ret[$tmpkey]['remote_name'] = $row['name'];
 				$ret[$tmpkey]['remote_object_id'] = $row['object_id'];
 				$ret[$tmpkey]['cableid'] = $cable;
+				$ret[$tmpkey]['cable_dict_key'] = $cable_dict_key;
 			}
 			$ret[$tmpkey]['remote_id'] = $remote_id;
 			unset ($result);
@@ -1232,19 +1235,44 @@ function getAllIPv4Allocations ()
 	return $ret;
 }
 
-function linkPorts ($porta, $portb, $cable = NULL)
+function linkPorts ($porta, $portb, $cable = NULL, $cable_type = NULL, $update = false)
 {
 	if ($porta == $portb)
 		throw new InvalidArgException ('porta/portb', $porta, "Ports can't be the same");
 
 	global $dbxlink;
-	$dbxlink->exec ('LOCK TABLES Link WRITE');
+	# the many locks are necessary. otherwise commitUnlinkPort could not access these tables:
+	$dbxlink->exec ('LOCK TABLES Link WRITE, Port AS pa WRITE, Port AS pb WRITE , RackObject WRITE');
 	$result = usePreparedSelectBlade
 	(
-		'SELECT COUNT(*) FROM Link WHERE porta IN (?,?) OR portb IN (?,?)',
+		'SELECT * FROM Link WHERE porta IN (?,?) OR portb IN (?,?)',
 		array ($porta, $portb, $porta, $portb)
 	);
-	if ($result->fetchColumn () != 0)
+	if ( $update )
+	{
+		$a = $result->fetch();
+		if( $a ){
+			if( ($a['porta'] == $porta && $a['portb'] == $portb) || ($a['portb'] == $porta && $a['porta'] == $portb) )
+			{
+				// update!
+				$dbxlink->exec ('UNLOCK TABLES');
+				usePreparedUpdateBlade(
+					'Link',
+					array (
+						'cable' => ( mb_strlen ($cable) ? $cable : NULL ) ,
+						'cable_dict_key' => ( mb_strlen ($cable_type) ? intval($cable_type) : NULL )
+					),
+					array( 'porta' => $a['porta'], 'portb' => $a['portb'] ) 
+				);
+				
+				return ;
+			}else{
+				commitUnlinkPort($porta);
+				commitUnlinkPort($portb);
+			}
+		}
+	}
+	else if ($result->fetch ())
 	{
 		$dbxlink->exec ('UNLOCK TABLES');
 		return "Port ${porta} or ${portb} is already linked";
@@ -1263,7 +1291,8 @@ function linkPorts ($porta, $portb, $cable = NULL)
 		(
 			'porta' => $porta,
 			'portb' => $portb,
-			'cable' => mb_strlen ($cable) ? $cable : NULL
+			'cable' => mb_strlen ($cable) ? $cable : NULL,
+			'cable_dict_key' => mb_strlen ($cable_type) ? intval($cable_type) : NULL
 		)
 	);
 	$dbxlink->exec ('UNLOCK TABLES');
@@ -2311,9 +2340,10 @@ function commitUpdateUserAccount ($id, $new_username, $new_realname, $new_passwo
 function getPortOIFCompat ()
 {
 	$query =
-		"select type1, type2, d1.dict_value as type1name, d2.dict_value as type2name from " .
+		"select type1, type2, d1.dict_value as type1name, d2.dict_value as type2name, cable_chapter_id , c.name as cable_chapter_name from " .
 		"PortCompat as pc inner join Dictionary as d1 on pc.type1 = d1.dict_key " .
 		"inner join Dictionary as d2 on pc.type2 = d2.dict_key " .
+		"left join Chapter as c on pc.cable_chapter_id = c.id ".
 		'ORDER BY type1name, type2name';
 	$result = usePreparedSelectBlade ($query);
 	return $result->fetchAll (PDO::FETCH_ASSOC);
diff --git a/wwwroot/inc/interface.php b/wwwroot/inc/interface.php
index 3e54e38..85fb28d 100644
--- a/wwwroot/inc/interface.php
+++ b/wwwroot/inc/interface.php
@@ -1164,7 +1164,24 @@ function renderPortsForObject ($object_id)
 					'object_id'=>$object_id)).
 			"'>";
 			printImageHREF ('cut', 'Unlink this port');
-			echo "</a></td>";
+			echo "</a>";
+			echo "<span";
+			$helper_args = array
+			(
+				'port' => $port['id'],
+			);
+			$popup_args = 'height=700, width=400, location=no, menubar=no, '.
+				'resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
+			echo " ondblclick='window.open(\"" . makeHrefForHelper ('portlist', $helper_args);
+			echo "\",\"findlink\",\"${popup_args}\");'";
+			// end of onclick=
+			echo " onclick='window.open(\"" . makeHrefForHelper ('portlist', $helper_args);
+			echo "\",\"findlink\",\"${popup_args}\");'";
+			// end of onclick=
+			echo '>';
+			// end of <a>
+			printImageHREF ('plug', 'Link this port');
+			echo "</span></td>";
 		}
 		elseif (strlen ($port['reservation_comment']))
 		{
@@ -3928,7 +3945,7 @@ function renderPortOIFCompatViewer()
 	$order = 'odd';
 	$last_left_oif_id = NULL;
 	echo '<br><table class=cooltable border=0 cellpadding=5 cellspacing=0 align=center>';
-	echo '<tr><th>From interface</th><th>To interface</th></tr>';
+	echo '<tr><th>From interface</th><th>To interface</th><th>Cable</th></tr>';
 	foreach (getPortOIFCompat() as $pair)
 	{
 		if ($last_left_oif_id != $pair['type1'])
@@ -3936,14 +3953,20 @@ function renderPortOIFCompatViewer()
 			$order = $nextorder[$order];
 			$last_left_oif_id = $pair['type1'];
 		}
-		echo "<tr class=row_${order}><td>${pair['type1name']}</td><td>${pair['type2name']}</td></tr>";
+		echo "<tr class=row_${order}><td>${pair['type1name']}</td><td>${pair['type2name']}</td><td>${pair['cable_chapter_name']}</td></tr>";
 	}
 	echo '</table>';
 }
 
 function renderPortOIFCompatEditor()
 {
-	function printNewitemTR()
+	$raw_chaplist = getChapterList();
+	$chaplist = array();
+	$chaplist[0]='';
+	foreach( $raw_chaplist as $chapter ){
+		$chaplist[$chapter['id']]= htmlspecialchars( $chapter['name'] );
+	}
+	function printNewitemTR($chaplist_local)
 	{
 		printOpFormIntro ('add');
 		echo '<tr><th class=tdleft>';
@@ -3952,6 +3975,10 @@ function renderPortOIFCompatEditor()
 		printSelect (readChapter (CHAP_PORTTYPE), array ('name' => 'type1'));
 		echo '</th><th class=tdleft>';
 		printSelect (readChapter (CHAP_PORTTYPE), array ('name' => 'type2'));
+		echo '</th><th class=tdleft>';
+		printSelect( $chaplist_local, array( 'name' => 'cable_chapter_id' ) );
+		echo '</th><th>';
+		printImageHREF ('add', 'add pair', TRUE);
 		echo '</th></tr></form>';
 	}
 
@@ -3959,9 +3986,9 @@ function renderPortOIFCompatEditor()
 	$last_left_oif_id = NULL;
 	$order = 'odd';
 	echo '<br><table class=cooltable align=center border=0 cellpadding=5 cellspacing=0>';
-	echo '<tr><th>&nbsp;</th><th>From Interface</th><th>To Interface</th></tr>';
+	echo '<tr><th>&nbsp;</th><th>From Interface</th><th>To Interface</th><th>Cable</th><th></th></tr>';
 	if (getConfigVar ('ADDNEW_AT_TOP') == 'yes')
-		printNewitemTR();
+		printNewitemTR($chaplist);
 	foreach (getPortOIFCompat() as $pair)
 	{
 		if ($last_left_oif_id != $pair['type1'])
@@ -3969,13 +3996,19 @@ function renderPortOIFCompatEditor()
 			$order = $nextorder[$order];
 			$last_left_oif_id = $pair['type1'];
 		}
+		printOpFormIntro('upd',array('type1'=>$pair['type1'], 'type2' => $pair['type2']) );
 		echo "<tr class=row_${order}><td>";
 		echo '<a href="' . makeHrefProcess (array ('op' => 'del', 'type1' => $pair['type1'], 'type2' => $pair['type2'])) . '">';
 		printImageHREF ('delete', 'remove pair');
-		echo "</a></td><td class=tdleft>${pair['type1name']}</td><td class=tdleft>${pair['type2name']}</td></tr>";
+		echo "</a></td><td class=tdleft>${pair['type1name']}</td><td class=tdleft>${pair['type2name']}</td>";
+		echo '<td>';
+		printSelect($chaplist, array('name'=>'cable_chapter_id'), $pair['cable_chapter_id']);
+		echo '</td><td>';
+		printImageHREF ('save', 'Save changes', TRUE); 
+		echo "</td></tr></form>";
 	}
 	if (getConfigVar ('ADDNEW_AT_TOP') != 'yes')
-		printNewitemTR();
+		printNewitemTR($chaplist);
 	echo '</table>';
 }
 
diff --git a/wwwroot/inc/navigation.php b/wwwroot/inc/navigation.php
index 7e25ec3..f32fdfc 100644
--- a/wwwroot/inc/navigation.php
+++ b/wwwroot/inc/navigation.php
@@ -453,6 +453,9 @@ $tabhandler['portmap']['default'] = 'renderPortOIFCompatViewer';
 $tabhandler['portmap']['edit'] = 'renderPortOIFCompatEditor';
 $ophandler['portmap']['edit']['add'] = 'tableHandler';
 $ophandler['portmap']['edit']['del'] = 'tableHandler';
+$ophandler['portmap']['edit']['upd'] = 'tableHandler';
+$ophandler['portmap']['edit']['addPack'] = 'addOIFCompatPack';
+$ophandler['portmap']['edit']['delPack'] = 'delOIFCompatPack';
 
 $page['portifcompat']['title'] = 'Enabled port types';
 $page['portifcompat']['parent'] = 'config';
diff --git a/wwwroot/inc/ophandlers.php b/wwwroot/inc/ophandlers.php
index 2fad512..3fc2e15 100644
--- a/wwwroot/inc/ophandlers.php
+++ b/wwwroot/inc/ophandlers.php
@@ -272,6 +272,21 @@ $opspec_list['portmap-edit-add'] = array
 	(
 		array ('url_argname' => 'type1', 'assertion' => 'uint'),
 		array ('url_argname' => 'type2', 'assertion' => 'uint'),
+		array ('url_argname' => 'cable_chapter_id', 'assertion' => 'uint0', 'if_empty' => 'NULL' )
+	),
+);
+$opspec_list['portmap-edit-upd'] = array
+(
+	'table' => 'PortCompat',
+	'action' => 'UPDATE',
+	'set_arglist' => array
+	(
+		array ('url_argname' => 'cable_chapter_id', 'assertion' => 'uint0', 'if_empty' => 'NULL'),
+	),
+	'where_arglist' => array
+	(
+		array ('url_argname' => 'type1', 'assertion' => 'uint'),
+		array ('url_argname' => 'type2', 'assertion' => 'uint')
 	),
 );
 $opspec_list['portmap-edit-del'] = array
diff --git a/wwwroot/inc/popup.php b/wwwroot/inc/popup.php
index 15244cb..d9bb8a8 100644
--- a/wwwroot/inc/popup.php
+++ b/wwwroot/inc/popup.php
@@ -34,7 +34,7 @@ function getProximateRacks ($rack_id, $proximity = 0)
 	return $ret;
 }
 
-function findSparePorts ($port_info, $filter)
+function getSparePorts($port_info, $filter)
 {
 	$qparams = array ();
 	$query = "
@@ -45,37 +45,34 @@ SELECT
 	p.iif_id,
 	p.type as oif_id,
 	pii.iif_name,
-	d.dict_value as oif_name,
 	p.object_id,
 	o.name as object_name
 FROM Port p
 INNER JOIN RackObject o ON o.id = p.object_id
 INNER JOIN PortInnerInterface pii ON p.iif_id = pii.id
-INNER JOIN Dictionary d ON d.dict_key = p.type
 ";
 	// porttype filter (non-strict match)
 	$query .= "
 INNER JOIN (
-	SELECT Port.id FROM Port
+	SELECT p2.id FROM Port p2
 	INNER JOIN
 	(
-		SELECT DISTINCT	pic2.iif_id
+		SELECT DISTINCT	pic2.iif_id, pic2.oif_id
 		FROM PortInterfaceCompat pic2
 		INNER JOIN PortCompat pc ON pc.type2 = pic2.oif_id
 ";
 		if ($port_info['iif_id'] != 1)
 		{
-			$query .= " INNER JOIN PortInterfaceCompat pic ON pic.oif_id = pc.type1 WHERE pic.iif_id = ? AND ";
+			$query .= " INNER JOIN PortInterfaceCompat pic ON pic.oif_id = pc.type1 WHERE pic.iif_id = ? ";
 			$qparams[] = $port_info['iif_id'];
 		}
 		else
 		{
-			$query .= " WHERE pc.type1 = ? AND ";
+			$query .= " WHERE pc.type1 = ? ";
 			$qparams[] = $port_info['oif_id'];
 		}
 		$query .= "
-			pic2.iif_id <> 1
-	) AS sub1 USING (iif_id)
+	) AS sub1 ON p2.iif_id = sub1.iif_id AND ( p2.iif_id <> 1 OR p2.type = sub1.oif_id )
 	UNION
 	SELECT Port.id
 	FROM Port
@@ -113,42 +110,104 @@ INNER JOIN (
 	// ordering
 	$query .= ' ORDER BY o.name';
 
-	$ret = array();
-	$result = usePreparedSelectBlade ($query, $qparams);
-	
-	$rows_by_pn = array();
-	$prev_object_id = NULL;
+	return usePreparedSelectBlade ($query, $qparams);
+}
+
+function getLinkPortJavascript($port_info, $ports)
+{
+	$required_oif = array();
+	$required_iif = array();
+	$required_chapter = array();
+	if( count($ports) == 0 ){
+		return '';
+	}
+	$jsports = array();
+	$compat = array();
+	$buffer = array('$(function($){');
+	$buffer[]='var PORT = { id:'.$port_info['id'].', name:"'.addslashes($port_info['name']).'",';
+	$buffer[]='	object_name:"'.addslashes($port_info['object_name']).'",';
+	$buffer[]='	oif_id:'.($port_info['oif_id'] ? $port_info['oif_id'] : 'null').', ';
+	$buffer[]='	iif_id:'.$port_info['iif_id'].', ';
+	$buffer[]='	cable:"'.addslashes($port_info['cableid']).'", ';
+	$buffer[]='	cable_dict_key:'.($port_info['cable_dict_key'] ? $port_info['cable_dict_key'] : 'null' ).', ';
+	$buffer[]='	remote_id:'.($port_info['remote_id'] ? $port_info['remote_id'] : 'null').'};';
+	$buffer[]='var PORTS = [';
 	
-	// fetch port rows from the DB
-	while (TRUE)
+	$required_iif[$port_info['iif_id']]=true;	
+	if( $port_info['oif_id'] ) $required_oif[$port_info['oif_id']]=true;
+	foreach( $ports as $port ){
+		$required_iif[ $port['iif_id'] ]=true;
+		if( $port['oif_id'] ) $required_oif[$port['oif_id']]=true;
+		$buffer[]='  { id:'.$port['id'].', name:"'.addslashes($port['name']).'", object_name:"'.addslashes($port['object_name']).'", oif_id:'.($port['oif_id'] ? $port['oif_id'] : 'null').', iif_id:'.$port['iif_id'].', reservation:"'.addslashes($port['reservation_comment']).'"},';
+	}
+	$buffer[]='];';
+
+	$incom = usePreparedSelectBlade('SELECT * FROM PortInterfaceCompat WHERE iif_id IN ('.questionMarks(count($required_iif)).') ORDER BY iif_id ', array_keys($required_iif));
+	$buffer[]='var INNER_OUTER_COMPATIBILITY = {';
+	$last_iif_id = null;
+	foreach( $incom as $in )
 	{
-		$row = $result->fetch (PDO::FETCH_ASSOC);
-		if (isset ($prev_object_id) and (! $row or $row['object_id'] != $prev_object_id))
+		if( $in['iif_id'] != $last_iif_id )
 		{
-			// handle sorted object's portlist
-			foreach (sortPortList ($rows_by_pn) as $ports_subarray)
-				foreach ($ports_subarray as $port_row)
-				{
-					$port_description = $port_row['object_name'] . ' --  ' . $port_row['name'];
-					if (count ($ports_subarray) > 1)
-					{
-						$if_type = $port_row['iif_id'] == 1 ? $port_row['oif_name'] : $port_row['iif_name'];
-						$port_description .= " ($if_type)";
-					}
-					if (! empty ($port_row['reservation_comment']))
-						$port_description .= '  --  ' . $port_row['reservation_comment'];
-					$ret[$port_row['id']] = $port_description;
-				}
-			$rows_by_pn = array();
+			if( $last_iif_id != null )
+			{
+				$buffer[]='  ],';
+			}
+			$buffer[]='  '.$in['iif_id'].': [';
+			$last_iif_id = $in['iif_id'];
 		}
-		$prev_object_id = $row['object_id'];
-		if ($row)
-			$rows_by_pn[$row['name']][] = $row;
-		else
-			break;
+		$required_oif[$in['oif_id']]=true;
+		$buffer[]='    '.$in['oif_id'].',';
 	}
+	if( $last_iif_id != null )
+	{
+		$buffer[]='  ]';
+	}
+	$buffer[]='};';
 
-	return $ret;
+	$query ='SELECT * FROM PortCompat pc'.
+		' WHERE type1 IN ('.questionMarks(count($required_oif)).') AND type2 IN ('.questionMarks(count($required_oif)).');';
+	$types = usePreparedSelectBlade($query, array_merge( array_keys($required_oif), array_keys($required_oif) ) );
+	foreach( $types as $type ){
+		@$compat[ $type['type1'] ][ $type['type2'] ] = array( 'cable_chapter_id'=> ($type['cable_chapter_id'] ? intval($type['cable_chapter_id']) : 'null') );
+		if( $type['cable_chapter_id'] ) $required_chapter[intval($type['cable_chapter_id'])] = true;
+	}
+	$buffer[]='var OUTER_COMPATIBILITY = {';
+	foreach( $compat as $a => $sub ){
+		$buffer[]='  '.$a.': { ';
+		foreach( $sub as $b => $info ){
+			$buffer[]='    '.$b.': {cable_chapter_id:'.$info['cable_chapter_id'].'},';
+		}
+		$buffer[]='  },';
+	}
+	$buffer[]='};';
+	if(	$required_chapter ){
+		$buffer[]='var CABLES = {';
+		$last_chapter_id = -1;
+		foreach( array_keys($required_chapter) as $chapter_id ){
+			$chapter = cookOptgroups( readChapter( $chapter_id, 'o' ) );
+			$buffer[]= $chapter_id.': [';
+			foreach( $chapter as $group => $entries ){
+				$buffer[]= '  {name: "'.addslashes($group).'", values:[';
+				foreach( $entries as $key => $value ){
+					$buffer[]= '    {key: '.$key.', value: "'.addslashes($value).'"},';
+				}
+				$buffer[]='  ]},';
+			}
+			$buffer[]='],';
+		}
+		$buffer[]='};';
+	}else{
+		$buffer[]='var CABLES = {};';
+	}
+	$oifs = usePreparedSelectBlade('SELECT dict_key,dict_value FROM Dictionary WHERE chapter_id = 2 AND dict_key IN ('.questionMarks(count($required_oif)).');', array_keys($required_oif));
+	$buffer[]='var OIF = {};';
+	foreach( $oifs as $oif ){
+		$buffer[]='OIF['.$oif['dict_key'].']= "'.addslashes($oif['dict_value']).'";';
+	}
+	$buffer[]='  initializeLinkportForm( PORT, PORTS, INNER_OUTER_COMPATIBILITY, OUTER_COMPATIBILITY, CABLES, OIF )';
+	$buffer[]='});';
+	return join("\n",$buffer);
 }
 
 // Return a list of all objects which are possible parents
@@ -221,131 +280,66 @@ function renderPopupObjectSelector()
 
 function handlePopupPortLink()
 {
+	global $dbxlink;
 	assertUIntArg ('port');
 	assertUIntArg ('remote_port');
 	assertStringArg ('cable', TRUE);
 	$port_info = getPortInfo ($_REQUEST['port']);
 	$remote_port_info = getPortInfo ($_REQUEST['remote_port']);
-	$POIFC = getPortOIFCompat();
-	if (isset ($_REQUEST['port_type']) and isset ($_REQUEST['remote_port_type']))
-	{
-		$type_local = $_REQUEST['port_type'];
-		$type_remote = $_REQUEST['remote_port_type'];
-	}
-	else
-	{
-		$type_local = $port_info['oif_id'];
-		$type_remote = $remote_port_info['oif_id'];
-	}
-	$matches = FALSE;
-	$js_table = '';
-	foreach ($POIFC as $pair)
-		if ($pair['type1'] == $type_local && $pair['type2'] == $type_remote)
-		{
-			$matches = TRUE;
-			break;
+	$dbxlink->beginTransaction();
+	try{
+		
+		if( $port_info['iif_id'] != 1 ){
+			// updateable
+			assertUIntArg ('local_oif');
+			if( $_POST['local_oif'] != $port_info['oif_id'] ){
+				commitUpdatePortOIF( $port_info['id'], $_POST['local_oif']);
+				$port_info['oif_id'] = intval($_POST['local_oif']);
+			}
 		}
-		else
-			$js_table .= "POIFC['${pair['type1']}-${pair['type2']}'] = 1;\n";
-
-	if ($matches)
-	{
-		if ($port_info['oif_id'] != $type_local)
-			commitUpdatePortOIF ($port_info['id'], $type_local);
-		if ($remote_port_info['oif_id'] != $type_remote)
-			commitUpdatePortOIF ($remote_port_info['id'], $type_remote);
-		linkPorts ($port_info['id'], $remote_port_info['id'], $_REQUEST['cable']);
-		showSuccess
-		(
-			sprintf
+		if( $remote_port_info['iif_id'] != 1 ){
+			// updateable
+			assertUIntArg ('remote_oif');
+			if( $_POST['remote_oif'] != $remote_port_info['oif_id'] ){
+				commitUpdatePortOIF( $remote_port_info['id'], $_POST['remote_oif']);
+				$remote_port_info['oif_id'] = intval($_POST['remote_oif']);
+			}
+		}
+		// now check compat
+		$compat = usePreparedSelectBlade('SELECT * FROM PortCompat WHERE ( type1 = ? AND type2 = ? ) OR ( type2 = ? AND type1= ? ) ',
+			array( $port_info['oif_id'],$remote_port_info['oif_id'],$port_info['oif_id'],$remote_port_info['oif_id']) )->fetchAll();
+		if( !$compat ){
+			throw new InvalidRequestArgException('oif_id', '', 'Ports are not compatible');	
+		}else{
+			// :)
+			$cable_type = null;
+			if( $compat[0]['cable_chapter_id'] && $_POST['cable_type'] ){
+				assertUIntArg('cable_type',true);
+				$cable_type = $_POST['cable_type'];
+			}
+			linkPorts( $port_info['id'], $remote_port_info['id'], $_POST['cable'], $cable_type, true );
+			$dbxlink->commit();
+			showSuccess
 			(
+			sprintf
+				(
 				'Port %s successfully linked with port %s',
 				formatPortLink ($port_info['id'], $port_info['name'], NULL, NULL),
 				formatPort ($remote_port_info)
-			)
-		);
-		addJS (<<<END
-window.opener.location.reload(true);
-window.close();
+				)
+			);
+			addJS (<<<END
+	window.opener.location.reload(true);
+	window.close();
 END
-		, TRUE);
-	}
-	else
-	{
-		// JS code to display port compatibility hint
-		addJS (<<<END
-POIFC = {};
-$js_table
-$(document).ready(function () {
-	$('select.porttype').change(onPortTypeChange);	
-	onPortTypeChange();
-});
-function onPortTypeChange() {
-	var key = $('*[name=port_type]')[0].value + '-' + $('*[name=remote_port_type]')[0].value;
-	if (POIFC[key] == 1)
-	{
-		$('#hint-not-compat').hide();
-		$('#hint-compat').show();
-	}
-	else
-	{
-		$('#hint-compat').hide();
-		$('#hint-not-compat').show();
-	}
-}
-END
-		, TRUE);
-		addCSS (<<<END
-.compat-hint {
-	display: none;
-	font-size: 125%;
-}
-.compat-hint#hint-compat {
-	color: green;
-}
-.compat-hint#hint-not-compat {
-	color: #804040;
-}
-END
-		, TRUE);
-		// render port type editor form
-		echo '<form method=GET>';
-		echo '<input type=hidden name="module" value="popup">';
-		echo '<input type=hidden name="helper" value="portlist">';
-		echo '<input type=hidden name="port" value="' . $port_info['id'] . '">';
-		echo '<input type=hidden name="remote_port" value="' . $remote_port_info['id'] . '">';
-		echo '<input type=hidden name="cable" value="' . htmlspecialchars ($_REQUEST['cable'], ENT_QUOTES) . '">';
-		echo '<p>The ports you have selected are not compatible. Please select a compatible transceiver pair.';
-		echo '<p>';
-		echo formatPort ($port_info) . ' ';
-		if ($port_info['iif_id'] == 1)
-		{
-			echo formatPortIIFOIF ($port_info);
-			echo '<input type=hidden name="port_type" value="' . $port_info['oif_id'] . '">';
-		}
-		else
-		{
-			echo '<label>' . $port_info['iif_name'] . ' ';
-			printSelect (getExistingPortTypeOptions ($port_info['id']), array ('class' => 'porttype', 'name' => 'port_type'), $type_local);
-			echo '</label>';
+			, true);
+			return;
 		}
-		echo ' &mdash; ';
-		if ($remote_port_info['iif_id'] == 1)
-		{
-			echo formatPortIIFOIF ($remote_port_info);
-			echo '<input type=hidden name="remote_port_type" value="' . $remote_port_info['oif_id'] . '">';
-		}
-		else
-		{
-			echo '<label>' . $remote_port_info['iif_name'] . ' ';
-			printSelect (getExistingPortTypeOptions ($remote_port_info['id']), array ('class' => 'porttype', 'name' => 'remote_port_type'), $type_remote);
-			echo '</label>';
-		}
-		echo ' ' . formatPort ($remote_port_info);
-		echo '<p class="compat-hint" id="hint-not-compat">&#10005; Not compatible port types</p>';
-		echo '<p class="compat-hint" id="hint-compat">&#10004; Compatible port types</p>';
-		echo '<p><input type=submit name="do_link" value="Link">';
+	}catch( Exception $e ){
+		$dbxlink->rollBack();
+		throw $e;
 	}
+	return renderPopupPortSelector();
 }
 
 function renderPopupPortSelector()
@@ -353,6 +347,7 @@ function renderPopupPortSelector()
 	assertUIntArg ('port');
 	$port_id = $_REQUEST['port'];
 	$port_info = getPortInfo ($port_id);
+
 	$in_rack = isset ($_REQUEST['in_rack']);
 
 	// fill port filter structure
@@ -379,11 +374,34 @@ function renderPopupPortSelector()
 		! empty ($filter['objects']) ||
 		! empty ($filter['ports'])
 	)
-		$spare_ports = findSparePorts ($port_info, $filter);
+	$ports_with_current = $ports = getSparePorts($port_info, $filter)->fetchAll();
+	if( $port_info['linked'] ){
+		$link = usePreparedSelectBlade('SELECT * FROM Link WHERE porta = ? OR portb = ?', array($port_id, $port_id))->fetch(PDO::FETCH_ASSOC);
+		
+		$current = getPortInfo( ($link['porta'] == $port_id) ? $link['portb'] : $link['porta'] );
+		$port_info['cableid'] = $current['cableid'] = $link['cable'];
+		$port_info['cable_dict_key'] = $current['cable_dict_key'] = $link['cable_dict_key'];
+		$port_info['remote_id'] = $current['id'];
 
+		$ports_with_current[] = $current;
+	}
+	addJS('js/link_port_form.js');
+	addJS(getLinkPortJavascript($port_info, $ports_with_current),true);
+	addCSS (<<<'END'
+.reserved {
+	text-decoration: line-through;
+}
+.compatible {
+	background: #afa;
+}
+.incompatible {
+	background: #faa;
+}
+END
+		, TRUE);
 	// display search form
 	echo 'Link ' . formatPort ($port_info) . ' to...';
-	echo '<form method=GET>';
+	echo '<form method="POST">';
 	startPortlet ('Port list filter');
 	echo '<input type=hidden name="module" value="popup">';
 	echo '<input type=hidden name="helper" value="portlist">';
@@ -391,23 +409,59 @@ function renderPopupPortSelector()
 	echo '<table align="center" valign="bottom"><tr>';
 	echo '<td class="tdleft"><label>Object name:<br><input type=text size=8 name="filter-obj" value="' . htmlspecialchars ($filter['objects'], ENT_QUOTES) . '"></label></td>';
 	echo '<td class="tdleft"><label>Port name:<br><input type=text size=6 name="filter-port" value="' . htmlspecialchars ($filter['ports'], ENT_QUOTES) . '"></label></td>';
+	echo '</tr><tr>';
 	echo '<td class="tdleft" valign="bottom"><label><input type=checkbox name="in_rack"' . ($in_rack ? ' checked' : '') . '>Nearest racks</label></td>';
 	echo '<td valign="bottom"><input type=submit value="show ports"></td>';
 	echo '</tr></table>';
 	finishPortlet();
 
 	// display results
-	startPortlet ('Compatible spare ports');
-	if (empty ($spare_ports))
-		echo '(nothing found)';
-	else
-	{
-		echo getSelect ($spare_ports, array ('name' => 'remote_port', 'size' => getConfigVar ('MAXSELSIZE')), NULL, FALSE);
-		echo "<p>Cable ID: <input type=text id=cable name=cable>";
-		echo "<p><input type='submit' value='Link' name='do_link'>";
+	startPortlet ('Link port');
+	if( $ports_with_current ){
+		echo '<div style="float: left;width:40%">';
+		echo '<select name="remote_port" id="remote_port" size="',getConfigVar ('MAXSELSIZE'),'" style="width:100%">';
+		if( $current ){
+			echo '<optgroup label="current">';
+			echo '<optgroup label="&nbsp; ', htmlspecialchars($current['object_name']), '">';
+			echo '<option value="',$current['id'],'" selected="selected">', htmlspecialchars($current['name'] ),'</option>';
+			echo '</optgroup>';
+		}
+		echo '<optgroup label="search result">';
+		if( !$ports ){
+			echo '<option disabled="disabled" value="">nothing found</option>';
+		}else{
+			$ports_by_object = array();
+			foreach( $ports as $port ){
+				if( !isset($ports_by_object[$port['object_name']]) ) $ports_by_object[$port['object_name']] = array();
+				$ports_by_object[$port['object_name']][] = $port;
+			}
+			ksort( $ports_by_object );
+			foreach( $ports_by_object as $name => $object_ports ){
+				echo '<optgroup label="&nbsp; ', htmlspecialchars($name), '">';
+				foreach( sortPortList($object_ports, true) as $port ){
+					echo '<option value="', $port['id'],'"',($port['reservation_comment'] ? ' class="reserved"':''),'>', htmlspecialchars($port['name']), '</option>';
+				}
+				echo '</optgroup>';
+			}
+		}
+		echo '</optgroup>';
+		echo '</select>';
+		echo '</div>';
+		echo '<div style="margin-left: 41%">';
+		echo '<p style="display:none">This port is reserved: <span id="reservation"></span></p>';
+		echo '<p style="display:none"><label for="cable_type">Local Transceiver</label></br><select name="local_oif" id="local_oif" style="width:100%"></select></p>';
+		echo '<p style="display:none"><label for="remote_oif">Remote Transceiver</label></br><select name="remote_oif" id="remote_oif" style="width:100%"></select></p>';
+		echo '<p style="display:none"><label for="cable_type">Cable type</label></br><select name="cable_type" id="cable_type" style="width:100%"></select></p>';
+		echo '<p><label for="cable">Cable ID: </label><br /><input type="text" id="cable" name="cable" style="width:100%" value="',htmlspecialchars($port_info['cableid']) ,'"/></p>';
+		echo '<p><input type="submit" value="Link" name="do_link" id="submit"></p>';
+		echo '<br style="clear:both" /></div>';
+	}else{
+		echo 'nothing found';
 	}
+
 	finishPortlet();
 	echo '</form>';
+	echo '<br style="clear:both" />';
 }
 
 function renderPopupIPv4Selector()
diff --git a/wwwroot/js/link_port_form.js b/wwwroot/js/link_port_form.js
new file mode 100644
index 0000000..0cf45a2
--- /dev/null
+++ b/wwwroot/js/link_port_form.js
@@ -0,0 +1,147 @@
+function initializeLinkportForm( PORT, PORTS, INNER_OUTER_COMPATIBILITY, OUTER_COMPATIBILITY, CABLES, OIF ){
+	var $remote_port = $('#remote_port'),$remote_oif = $('#remote_oif'),$local_oif = $('#local_oif'),$cable_type = $('#cable_type'), $submit = $('#submit'), $reservation = $('#reservation');
+	var compatClass = function(a,b){
+		if( !a || !b ){
+			return '';
+		}else if( !OUTER_COMPATIBILITY[a] || !OUTER_COMPATIBILITY[a][b]  ){
+			return 'incompatible';
+		}else{
+			return 'compatible';
+		}
+	}
+	var updateSelects = function(){
+		var rp = $remote_port.val(), ro = $remote_oif.val(), lo = $local_oif.val(), ct = $cable_type.val();
+		if( ro ) ro = parseInt(ro);
+		if( !lo ){
+			lo = PORT.oif_id;
+		}else{
+			lo = parseInt(lo);
+		}
+		if( !rp ){
+			rp = PORT.remote_id;
+		}
+		if( !ct ){
+			ct = PORT.cable_dict_key;
+		}
+		//remote_oif
+		$remote_oif.empty();
+		var remote_port;
+		if( rp ){
+			remote_port =	$.grep(PORTS,function(p){ return p.id == rp })[0];
+			if( !ro || remote_port.iif_id == 1 || INNER_OUTER_COMPATIBILITY[ remote_port.iif_id ].indexOf(ro) == -1  ){
+				ro = remote_port.oif_id;
+			}
+			if( remote_port.reservation == "" ){
+				$reservation.closest('p').hide();
+			}else{
+				$reservation.text( remote_port.reservation );
+				$reservation.closest('p').show();	
+			}
+			if( remote_port.iif_id != 1 ){
+				$remote_oif.closest('p').show();	
+				$remote_oif.attr('disabled',false);
+			}else{
+				$remote_oif.closest('p').hide();
+				$remote_oif.attr('disabled',true);
+			}
+			var rps = INNER_OUTER_COMPATIBILITY[ remote_port.iif_id ];
+			rps.sort(function(a,b){
+				return OIF[a].localeCompare(OIF[b]);
+			}).forEach( function(oif){
+				$('<option>').val(oif).text( OIF[oif] ).addClass( compatClass( oif, lo )  ).appendTo($remote_oif);
+			});
+			$remote_oif.val(ro);
+		}else{
+			$remote_oif.closest('p').hide();	
+			$remote_oif.attr('disabled',true);
+		}
+		//local_oif
+		$local_oif.empty();
+		if( PORT.iif_id != 1 ){
+			$local_oif.closest('p').show();
+			$local_oif.attr('disabled',false);
+		}else{
+			$local_oif.closest('p').hide();
+			$local_oif.attr('disabled',true);	
+		}
+		var lps = INNER_OUTER_COMPATIBILITY[ PORT.iif_id ];
+		lps.sort(function(a,b){
+			return OIF[a].localeCompare(OIF[b]);
+		}).forEach( function(oif){
+			$('<option>').val(oif).text( OIF[oif] ).addClass( compatClass( oif, ro ) ).appendTo($local_oif);
+		});
+		$local_oif.val(lo);
+		if( rp && lo && ro ){
+			// cables and compat!
+			var comb = OUTER_COMPATIBILITY[ ro ] ? OUTER_COMPATIBILITY[ ro ][ lo ] : null;
+			if( !comb ){
+				$submit.attr('disabled',true);
+				$submit.val('Incompatible transceiver');
+				$cable_type.closest('p').hide();
+			}else{
+				if( comb.cable_chapter_id ){
+					$cable_type.empty();
+					var cables = CABLES[comb.cable_chapter_id];
+					if( cables ){
+						var other;
+						$.each(cables,function(i,group){
+							if( group.name == 'other' ){
+								other = group;
+								return ;
+							}
+							var g = $('<optgroup>').attr('label',group.name).appendTo($cable_type);
+							$.each(group.values, function(j, opt){
+								$('<option>').val(opt.key).text(opt.value).appendTo(g);
+							});
+						});
+						if( other ){
+							$.each(other.values, function(j, opt){
+								$('<option>').val(opt.key).text(opt.value).appendTo($cable_type);
+							});
+						}
+					}
+				 $('<option>').val('').text('').appendTo($cable_type);	
+					$cable_type.val(ct);
+					$cable_type.closest('p').show();
+				}else{
+					// compatible but no cables
+					$cable_type.closest('p').hide();
+				}
+				$submit.attr('disabled',false);
+				$submit.val('Link');
+			}
+		}else{
+			if( !rp ){
+				$submit.attr('disabled',true);
+				$submit.val('Select a port');	
+			}
+			$cable_type.closest('p').hide();
+		}
+	}
+	updateSelects();
+	$remote_port.change(updateSelects);
+	var changeTransceiver = function( $a, $b ){
+		var av = $a.val();
+		var bv = $b.val();
+		if( !OUTER_COMPATIBILITY[av] ){
+			return ;
+		}
+		if(	OUTER_COMPATIBILITY[av][bv] ){
+			return;
+		}
+		$b.children('option').each(function(i,o ){
+			if( OUTER_COMPATIBILITY[ av ][ $(o).val() ] ){
+				$b.val( $(o).val() );
+				return false;
+			}
+		});
+	}
+	$local_oif.change(function(){
+		changeTransceiver($local_oif,$remote_oif);
+		updateSelects();
+	});
+	$remote_oif.change(function(){
+		changeTransceiver($remote_oif,$local_oif);
+		updateSelects();
+	}); 
+}
track_cable_type_0.19.13.diff (31,959 bytes)   
track_cable_type_0.19.13.correct.diff (32,223 bytes)   
diff --git a/wwwroot/inc/database.php b/wwwroot/inc/database.php
index d9f8e2c..1b9dd82 100644
--- a/wwwroot/inc/database.php
+++ b/wwwroot/inc/database.php
@@ -580,9 +580,10 @@ function getObjectPortsAndLinks ($object_id)
 	{
 		$portid = $ret[$tmpkey]['id'];
 		$remote_id = NULL;
-		$query = "select porta, portb, cable from Link where porta = ? or portb = ?";
+		$query = "select porta, portb, cable, cable_dict_key from Link where porta = ? or portb = ?";
 		$result = usePreparedSelectBlade ($query, array ($portid, $portid));
 		$cable = "CableID n/a";
+		$cable_dict_key = null;
 		if ($row = $result->fetch (PDO::FETCH_ASSOC))
 		{
 			if ($portid != $row['porta'])
@@ -590,6 +591,7 @@ function getObjectPortsAndLinks ($object_id)
 			elseif ($portid != $row['portb'])
 				$remote_id = $row['portb'];
 			$cable = $row['cable'];
+			$cable_dict_key = $row['cable_dict_key'];
 		}
 		unset ($result);
 		if ($remote_id) // there's a remote end here
@@ -601,6 +603,7 @@ function getObjectPortsAndLinks ($object_id)
 				$ret[$tmpkey]['remote_name'] = $row['name'];
 				$ret[$tmpkey]['remote_object_id'] = $row['object_id'];
 				$ret[$tmpkey]['cableid'] = $cable;
+				$ret[$tmpkey]['cable_dict_key'] = $cable_dict_key;
 			}
 			$ret[$tmpkey]['remote_id'] = $remote_id;
 			unset ($result);
@@ -1232,19 +1235,44 @@ function getAllIPv4Allocations ()
 	return $ret;
 }
 
-function linkPorts ($porta, $portb, $cable = NULL)
+function linkPorts ($porta, $portb, $cable = NULL, $cable_type = NULL, $update = false)
 {
 	if ($porta == $portb)
 		throw new InvalidArgException ('porta/portb', $porta, "Ports can't be the same");
 
 	global $dbxlink;
-	$dbxlink->exec ('LOCK TABLES Link WRITE');
+	# the many locks are necessary. otherwise commitUnlinkPort could not access these tables:
+	$dbxlink->exec ('LOCK TABLES Link WRITE, Port AS pa WRITE, Port AS pb WRITE , RackObject WRITE');
 	$result = usePreparedSelectBlade
 	(
-		'SELECT COUNT(*) FROM Link WHERE porta IN (?,?) OR portb IN (?,?)',
+		'SELECT * FROM Link WHERE porta IN (?,?) OR portb IN (?,?)',
 		array ($porta, $portb, $porta, $portb)
 	);
-	if ($result->fetchColumn () != 0)
+	if ( $update )
+	{
+		$a = $result->fetch();
+		if( $a ){
+			if( ($a['porta'] == $porta && $a['portb'] == $portb) || ($a['portb'] == $porta && $a['porta'] == $portb) )
+			{
+				// update!
+				$dbxlink->exec ('UNLOCK TABLES');
+				usePreparedUpdateBlade(
+					'Link',
+					array (
+						'cable' => ( mb_strlen ($cable) ? $cable : NULL ) ,
+						'cable_dict_key' => ( mb_strlen ($cable_type) ? intval($cable_type) : NULL )
+					),
+					array( 'porta' => $a['porta'], 'portb' => $a['portb'] ) 
+				);
+				
+				return ;
+			}else{
+				commitUnlinkPort($porta);
+				commitUnlinkPort($portb);
+			}
+		}
+	}
+	else if ($result->fetch ())
 	{
 		$dbxlink->exec ('UNLOCK TABLES');
 		return "Port ${porta} or ${portb} is already linked";
@@ -1263,7 +1291,8 @@ function linkPorts ($porta, $portb, $cable = NULL)
 		(
 			'porta' => $porta,
 			'portb' => $portb,
-			'cable' => mb_strlen ($cable) ? $cable : NULL
+			'cable' => mb_strlen ($cable) ? $cable : NULL,
+			'cable_dict_key' => mb_strlen ($cable_type) ? intval($cable_type) : NULL
 		)
 	);
 	$dbxlink->exec ('UNLOCK TABLES');
@@ -2311,9 +2340,10 @@ function commitUpdateUserAccount ($id, $new_username, $new_realname, $new_passwo
 function getPortOIFCompat ()
 {
 	$query =
-		"select type1, type2, d1.dict_value as type1name, d2.dict_value as type2name from " .
+		"select type1, type2, d1.dict_value as type1name, d2.dict_value as type2name, cable_chapter_id , c.name as cable_chapter_name from " .
 		"PortCompat as pc inner join Dictionary as d1 on pc.type1 = d1.dict_key " .
 		"inner join Dictionary as d2 on pc.type2 = d2.dict_key " .
+		"left join Chapter as c on pc.cable_chapter_id = c.id ".
 		'ORDER BY type1name, type2name';
 	$result = usePreparedSelectBlade ($query);
 	return $result->fetchAll (PDO::FETCH_ASSOC);
diff --git a/wwwroot/inc/interface.php b/wwwroot/inc/interface.php
index 3e54e38..85fb28d 100644
--- a/wwwroot/inc/interface.php
+++ b/wwwroot/inc/interface.php
@@ -1164,7 +1164,24 @@ function renderPortsForObject ($object_id)
 					'object_id'=>$object_id)).
 			"'>";
 			printImageHREF ('cut', 'Unlink this port');
-			echo "</a></td>";
+			echo "</a>";
+			echo "<span";
+			$helper_args = array
+			(
+				'port' => $port['id'],
+			);
+			$popup_args = 'height=700, width=400, location=no, menubar=no, '.
+				'resizable=yes, scrollbars=yes, status=no, titlebar=no, toolbar=no';
+			echo " ondblclick='window.open(\"" . makeHrefForHelper ('portlist', $helper_args);
+			echo "\",\"findlink\",\"${popup_args}\");'";
+			// end of onclick=
+			echo " onclick='window.open(\"" . makeHrefForHelper ('portlist', $helper_args);
+			echo "\",\"findlink\",\"${popup_args}\");'";
+			// end of onclick=
+			echo '>';
+			// end of <a>
+			printImageHREF ('plug', 'Link this port');
+			echo "</span></td>";
 		}
 		elseif (strlen ($port['reservation_comment']))
 		{
@@ -3928,7 +3945,7 @@ function renderPortOIFCompatViewer()
 	$order = 'odd';
 	$last_left_oif_id = NULL;
 	echo '<br><table class=cooltable border=0 cellpadding=5 cellspacing=0 align=center>';
-	echo '<tr><th>From interface</th><th>To interface</th></tr>';
+	echo '<tr><th>From interface</th><th>To interface</th><th>Cable</th></tr>';
 	foreach (getPortOIFCompat() as $pair)
 	{
 		if ($last_left_oif_id != $pair['type1'])
@@ -3936,14 +3953,20 @@ function renderPortOIFCompatViewer()
 			$order = $nextorder[$order];
 			$last_left_oif_id = $pair['type1'];
 		}
-		echo "<tr class=row_${order}><td>${pair['type1name']}</td><td>${pair['type2name']}</td></tr>";
+		echo "<tr class=row_${order}><td>${pair['type1name']}</td><td>${pair['type2name']}</td><td>${pair['cable_chapter_name']}</td></tr>";
 	}
 	echo '</table>';
 }
 
 function renderPortOIFCompatEditor()
 {
-	function printNewitemTR()
+	$raw_chaplist = getChapterList();
+	$chaplist = array();
+	$chaplist[0]='';
+	foreach( $raw_chaplist as $chapter ){
+		$chaplist[$chapter['id']]= htmlspecialchars( $chapter['name'] );
+	}
+	function printNewitemTR($chaplist_local)
 	{
 		printOpFormIntro ('add');
 		echo '<tr><th class=tdleft>';
@@ -3952,6 +3975,10 @@ function renderPortOIFCompatEditor()
 		printSelect (readChapter (CHAP_PORTTYPE), array ('name' => 'type1'));
 		echo '</th><th class=tdleft>';
 		printSelect (readChapter (CHAP_PORTTYPE), array ('name' => 'type2'));
+		echo '</th><th class=tdleft>';
+		printSelect( $chaplist_local, array( 'name' => 'cable_chapter_id' ) );
+		echo '</th><th>';
+		printImageHREF ('add', 'add pair', TRUE);
 		echo '</th></tr></form>';
 	}
 
@@ -3959,9 +3986,9 @@ function renderPortOIFCompatEditor()
 	$last_left_oif_id = NULL;
 	$order = 'odd';
 	echo '<br><table class=cooltable align=center border=0 cellpadding=5 cellspacing=0>';
-	echo '<tr><th>&nbsp;</th><th>From Interface</th><th>To Interface</th></tr>';
+	echo '<tr><th>&nbsp;</th><th>From Interface</th><th>To Interface</th><th>Cable</th><th></th></tr>';
 	if (getConfigVar ('ADDNEW_AT_TOP') == 'yes')
-		printNewitemTR();
+		printNewitemTR($chaplist);
 	foreach (getPortOIFCompat() as $pair)
 	{
 		if ($last_left_oif_id != $pair['type1'])
@@ -3969,13 +3996,19 @@ function renderPortOIFCompatEditor()
 			$order = $nextorder[$order];
 			$last_left_oif_id = $pair['type1'];
 		}
+		printOpFormIntro('upd',array('type1'=>$pair['type1'], 'type2' => $pair['type2']) );
 		echo "<tr class=row_${order}><td>";
 		echo '<a href="' . makeHrefProcess (array ('op' => 'del', 'type1' => $pair['type1'], 'type2' => $pair['type2'])) . '">';
 		printImageHREF ('delete', 'remove pair');
-		echo "</a></td><td class=tdleft>${pair['type1name']}</td><td class=tdleft>${pair['type2name']}</td></tr>";
+		echo "</a></td><td class=tdleft>${pair['type1name']}</td><td class=tdleft>${pair['type2name']}</td>";
+		echo '<td>';
+		printSelect($chaplist, array('name'=>'cable_chapter_id'), $pair['cable_chapter_id']);
+		echo '</td><td>';
+		printImageHREF ('save', 'Save changes', TRUE); 
+		echo "</td></tr></form>";
 	}
 	if (getConfigVar ('ADDNEW_AT_TOP') != 'yes')
-		printNewitemTR();
+		printNewitemTR($chaplist);
 	echo '</table>';
 }
 
diff --git a/wwwroot/inc/navigation.php b/wwwroot/inc/navigation.php
index 7e25ec3..f32fdfc 100644
--- a/wwwroot/inc/navigation.php
+++ b/wwwroot/inc/navigation.php
@@ -453,6 +453,9 @@ $tabhandler['portmap']['default'] = 'renderPortOIFCompatViewer';
 $tabhandler['portmap']['edit'] = 'renderPortOIFCompatEditor';
 $ophandler['portmap']['edit']['add'] = 'tableHandler';
 $ophandler['portmap']['edit']['del'] = 'tableHandler';
+$ophandler['portmap']['edit']['upd'] = 'tableHandler';
+$ophandler['portmap']['edit']['addPack'] = 'addOIFCompatPack';
+$ophandler['portmap']['edit']['delPack'] = 'delOIFCompatPack';
 
 $page['portifcompat']['title'] = 'Enabled port types';
 $page['portifcompat']['parent'] = 'config';
diff --git a/wwwroot/inc/ophandlers.php b/wwwroot/inc/ophandlers.php
index 2fad512..3fc2e15 100644
--- a/wwwroot/inc/ophandlers.php
+++ b/wwwroot/inc/ophandlers.php
@@ -272,6 +272,21 @@ $opspec_list['portmap-edit-add'] = array
 	(
 		array ('url_argname' => 'type1', 'assertion' => 'uint'),
 		array ('url_argname' => 'type2', 'assertion' => 'uint'),
+		array ('url_argname' => 'cable_chapter_id', 'assertion' => 'uint0', 'if_empty' => 'NULL' )
+	),
+);
+$opspec_list['portmap-edit-upd'] = array
+(
+	'table' => 'PortCompat',
+	'action' => 'UPDATE',
+	'set_arglist' => array
+	(
+		array ('url_argname' => 'cable_chapter_id', 'assertion' => 'uint0', 'if_empty' => 'NULL'),
+	),
+	'where_arglist' => array
+	(
+		array ('url_argname' => 'type1', 'assertion' => 'uint'),
+		array ('url_argname' => 'type2', 'assertion' => 'uint')
 	),
 );
 $opspec_list['portmap-edit-del'] = array
diff --git a/wwwroot/inc/popup.php b/wwwroot/inc/popup.php
index 15244cb..85cb212 100644
--- a/wwwroot/inc/popup.php
+++ b/wwwroot/inc/popup.php
@@ -34,7 +34,7 @@ function getProximateRacks ($rack_id, $proximity = 0)
 	return $ret;
 }
 
-function findSparePorts ($port_info, $filter)
+function getSparePorts($port_info, $filter)
 {
 	$qparams = array ();
 	$query = "
@@ -45,37 +45,34 @@ SELECT
 	p.iif_id,
 	p.type as oif_id,
 	pii.iif_name,
-	d.dict_value as oif_name,
 	p.object_id,
 	o.name as object_name
 FROM Port p
 INNER JOIN RackObject o ON o.id = p.object_id
 INNER JOIN PortInnerInterface pii ON p.iif_id = pii.id
-INNER JOIN Dictionary d ON d.dict_key = p.type
 ";
 	// porttype filter (non-strict match)
 	$query .= "
 INNER JOIN (
-	SELECT Port.id FROM Port
+	SELECT p2.id FROM Port p2
 	INNER JOIN
 	(
-		SELECT DISTINCT	pic2.iif_id
+		SELECT DISTINCT	pic2.iif_id, pic2.oif_id
 		FROM PortInterfaceCompat pic2
 		INNER JOIN PortCompat pc ON pc.type2 = pic2.oif_id
 ";
 		if ($port_info['iif_id'] != 1)
 		{
-			$query .= " INNER JOIN PortInterfaceCompat pic ON pic.oif_id = pc.type1 WHERE pic.iif_id = ? AND ";
+			$query .= " INNER JOIN PortInterfaceCompat pic ON pic.oif_id = pc.type1 WHERE pic.iif_id = ? ";
 			$qparams[] = $port_info['iif_id'];
 		}
 		else
 		{
-			$query .= " WHERE pc.type1 = ? AND ";
+			$query .= " WHERE pc.type1 = ? ";
 			$qparams[] = $port_info['oif_id'];
 		}
 		$query .= "
-			pic2.iif_id <> 1
-	) AS sub1 USING (iif_id)
+	) AS sub1 ON p2.iif_id = sub1.iif_id AND ( p2.iif_id <> 1 OR p2.type = sub1.oif_id )
 	UNION
 	SELECT Port.id
 	FROM Port
@@ -113,42 +110,104 @@ INNER JOIN (
 	// ordering
 	$query .= ' ORDER BY o.name';
 
-	$ret = array();
-	$result = usePreparedSelectBlade ($query, $qparams);
-	
-	$rows_by_pn = array();
-	$prev_object_id = NULL;
+	return usePreparedSelectBlade ($query, $qparams);
+}
+
+function getLinkPortJavascript($port_info, $ports)
+{
+	$required_oif = array();
+	$required_iif = array();
+	$required_chapter = array();
+	if( count($ports) == 0 ){
+		return '';
+	}
+	$jsports = array();
+	$compat = array();
+	$buffer = array('$(function($){');
+	$buffer[]='var PORT = { id:'.$port_info['id'].', name:"'.addslashes($port_info['name']).'",';
+	$buffer[]='	object_name:"'.addslashes($port_info['object_name']).'",';
+	$buffer[]='	oif_id:'.($port_info['oif_id'] ? $port_info['oif_id'] : 'null').', ';
+	$buffer[]='	iif_id:'.$port_info['iif_id'].', ';
+	$buffer[]='	cable:"'.addslashes($port_info['cableid']).'", ';
+	$buffer[]='	cable_dict_key:'.($port_info['cable_dict_key'] ? $port_info['cable_dict_key'] : 'null' ).', ';
+	$buffer[]='	remote_id:'.($port_info['remote_id'] ? $port_info['remote_id'] : 'null').'};';
+	$buffer[]='var PORTS = [';
 	
-	// fetch port rows from the DB
-	while (TRUE)
+	$required_iif[$port_info['iif_id']]=true;	
+	if( $port_info['oif_id'] ) $required_oif[$port_info['oif_id']]=true;
+	foreach( $ports as $port ){
+		$required_iif[ $port['iif_id'] ]=true;
+		if( $port['oif_id'] ) $required_oif[$port['oif_id']]=true;
+		$buffer[]='  { id:'.$port['id'].', name:"'.addslashes($port['name']).'", object_name:"'.addslashes($port['object_name']).'", oif_id:'.($port['oif_id'] ? $port['oif_id'] : 'null').', iif_id:'.$port['iif_id'].', reservation:"'.addslashes($port['reservation_comment']).'"},';
+	}
+	$buffer[]='];';
+
+	$incom = usePreparedSelectBlade('SELECT * FROM PortInterfaceCompat WHERE iif_id IN ('.questionMarks(count($required_iif)).') ORDER BY iif_id ', array_keys($required_iif));
+	$buffer[]='var INNER_OUTER_COMPATIBILITY = {';
+	$last_iif_id = null;
+	foreach( $incom as $in )
 	{
-		$row = $result->fetch (PDO::FETCH_ASSOC);
-		if (isset ($prev_object_id) and (! $row or $row['object_id'] != $prev_object_id))
+		if( $in['iif_id'] != $last_iif_id )
 		{
-			// handle sorted object's portlist
-			foreach (sortPortList ($rows_by_pn) as $ports_subarray)
-				foreach ($ports_subarray as $port_row)
-				{
-					$port_description = $port_row['object_name'] . ' --  ' . $port_row['name'];
-					if (count ($ports_subarray) > 1)
-					{
-						$if_type = $port_row['iif_id'] == 1 ? $port_row['oif_name'] : $port_row['iif_name'];
-						$port_description .= " ($if_type)";
-					}
-					if (! empty ($port_row['reservation_comment']))
-						$port_description .= '  --  ' . $port_row['reservation_comment'];
-					$ret[$port_row['id']] = $port_description;
-				}
-			$rows_by_pn = array();
+			if( $last_iif_id != null )
+			{
+				$buffer[]='  ],';
+			}
+			$buffer[]='  '.$in['iif_id'].': [';
+			$last_iif_id = $in['iif_id'];
 		}
-		$prev_object_id = $row['object_id'];
-		if ($row)
-			$rows_by_pn[$row['name']][] = $row;
-		else
-			break;
+		$required_oif[$in['oif_id']]=true;
+		$buffer[]='    '.$in['oif_id'].',';
 	}
+	if( $last_iif_id != null )
+	{
+		$buffer[]='  ]';
+	}
+	$buffer[]='};';
 
-	return $ret;
+	$query ='SELECT * FROM PortCompat pc'.
+		' WHERE type1 IN ('.questionMarks(count($required_oif)).') AND type2 IN ('.questionMarks(count($required_oif)).');';
+	$types = usePreparedSelectBlade($query, array_merge( array_keys($required_oif), array_keys($required_oif) ) );
+	foreach( $types as $type ){
+		@$compat[ $type['type1'] ][ $type['type2'] ] = array( 'cable_chapter_id'=> ($type['cable_chapter_id'] ? intval($type['cable_chapter_id']) : 'null') );
+		if( $type['cable_chapter_id'] ) $required_chapter[intval($type['cable_chapter_id'])] = true;
+	}
+	$buffer[]='var OUTER_COMPATIBILITY = {';
+	foreach( $compat as $a => $sub ){
+		$buffer[]='  '.$a.': { ';
+		foreach( $sub as $b => $info ){
+			$buffer[]='    '.$b.': {cable_chapter_id:'.$info['cable_chapter_id'].'},';
+		}
+		$buffer[]='  },';
+	}
+	$buffer[]='};';
+	if(	$required_chapter ){
+		$buffer[]='var CABLES = {';
+		$last_chapter_id = -1;
+		foreach( array_keys($required_chapter) as $chapter_id ){
+			$chapter = cookOptgroups( readChapter( $chapter_id, 'o' ) );
+			$buffer[]= $chapter_id.': [';
+			foreach( $chapter as $group => $entries ){
+				$buffer[]= '  {name: "'.addslashes($group).'", values:[';
+				foreach( $entries as $key => $value ){
+					$buffer[]= '    {key: '.$key.', value: "'.addslashes($value).'"},';
+				}
+				$buffer[]='  ]},';
+			}
+			$buffer[]='],';
+		}
+		$buffer[]='};';
+	}else{
+		$buffer[]='var CABLES = {};';
+	}
+	$oifs = usePreparedSelectBlade('SELECT dict_key,dict_value FROM Dictionary WHERE chapter_id = 2 AND dict_key IN ('.questionMarks(count($required_oif)).');', array_keys($required_oif));
+	$buffer[]='var OIF = {};';
+	foreach( $oifs as $oif ){
+		$buffer[]='OIF['.$oif['dict_key'].']= "'.addslashes($oif['dict_value']).'";';
+	}
+	$buffer[]='  initializeLinkportForm( PORT, PORTS, INNER_OUTER_COMPATIBILITY, OUTER_COMPATIBILITY, CABLES, OIF )';
+	$buffer[]='});';
+	return join("\n",$buffer);
 }
 
 // Return a list of all objects which are possible parents
@@ -221,131 +280,66 @@ function renderPopupObjectSelector()
 
 function handlePopupPortLink()
 {
+	global $dbxlink;
 	assertUIntArg ('port');
 	assertUIntArg ('remote_port');
 	assertStringArg ('cable', TRUE);
 	$port_info = getPortInfo ($_REQUEST['port']);
 	$remote_port_info = getPortInfo ($_REQUEST['remote_port']);
-	$POIFC = getPortOIFCompat();
-	if (isset ($_REQUEST['port_type']) and isset ($_REQUEST['remote_port_type']))
-	{
-		$type_local = $_REQUEST['port_type'];
-		$type_remote = $_REQUEST['remote_port_type'];
-	}
-	else
-	{
-		$type_local = $port_info['oif_id'];
-		$type_remote = $remote_port_info['oif_id'];
-	}
-	$matches = FALSE;
-	$js_table = '';
-	foreach ($POIFC as $pair)
-		if ($pair['type1'] == $type_local && $pair['type2'] == $type_remote)
-		{
-			$matches = TRUE;
-			break;
+	$dbxlink->beginTransaction();
+	try{
+		
+		if( $port_info['iif_id'] != 1 ){
+			// updateable
+			assertUIntArg ('local_oif');
+			if( $_POST['local_oif'] != $port_info['oif_id'] ){
+				commitUpdatePortOIF( $port_info['id'], $_POST['local_oif']);
+				$port_info['oif_id'] = intval($_POST['local_oif']);
+			}
 		}
-		else
-			$js_table .= "POIFC['${pair['type1']}-${pair['type2']}'] = 1;\n";
-
-	if ($matches)
-	{
-		if ($port_info['oif_id'] != $type_local)
-			commitUpdatePortOIF ($port_info['id'], $type_local);
-		if ($remote_port_info['oif_id'] != $type_remote)
-			commitUpdatePortOIF ($remote_port_info['id'], $type_remote);
-		linkPorts ($port_info['id'], $remote_port_info['id'], $_REQUEST['cable']);
-		showSuccess
-		(
-			sprintf
+		if( $remote_port_info['iif_id'] != 1 ){
+			// updateable
+			assertUIntArg ('remote_oif');
+			if( $_POST['remote_oif'] != $remote_port_info['oif_id'] ){
+				commitUpdatePortOIF( $remote_port_info['id'], $_POST['remote_oif']);
+				$remote_port_info['oif_id'] = intval($_POST['remote_oif']);
+			}
+		}
+		// now check compat
+		$compat = usePreparedSelectBlade('SELECT * FROM PortCompat WHERE ( type1 = ? AND type2 = ? ) OR ( type2 = ? AND type1= ? ) ',
+			array( $port_info['oif_id'],$remote_port_info['oif_id'],$port_info['oif_id'],$remote_port_info['oif_id']) )->fetchAll();
+		if( !$compat ){
+			throw new InvalidRequestArgException('oif_id', '', 'Ports are not compatible');	
+		}else{
+			// :)
+			$cable_type = null;
+			if( $compat[0]['cable_chapter_id'] && $_POST['cable_type'] ){
+				assertUIntArg('cable_type',true);
+				$cable_type = $_POST['cable_type'];
+			}
+			linkPorts( $port_info['id'], $remote_port_info['id'], $_POST['cable'], $cable_type, true );
+			$dbxlink->commit();
+			showSuccess
 			(
+			sprintf
+				(
 				'Port %s successfully linked with port %s',
 				formatPortLink ($port_info['id'], $port_info['name'], NULL, NULL),
 				formatPort ($remote_port_info)
-			)
-		);
-		addJS (<<<END
-window.opener.location.reload(true);
-window.close();
+				)
+			);
+			addJS (<<<END
+	window.opener.location.reload(true);
+	window.close();
 END
-		, TRUE);
-	}
-	else
-	{
-		// JS code to display port compatibility hint
-		addJS (<<<END
-POIFC = {};
-$js_table
-$(document).ready(function () {
-	$('select.porttype').change(onPortTypeChange);	
-	onPortTypeChange();
-});
-function onPortTypeChange() {
-	var key = $('*[name=port_type]')[0].value + '-' + $('*[name=remote_port_type]')[0].value;
-	if (POIFC[key] == 1)
-	{
-		$('#hint-not-compat').hide();
-		$('#hint-compat').show();
-	}
-	else
-	{
-		$('#hint-compat').hide();
-		$('#hint-not-compat').show();
-	}
-}
-END
-		, TRUE);
-		addCSS (<<<END
-.compat-hint {
-	display: none;
-	font-size: 125%;
-}
-.compat-hint#hint-compat {
-	color: green;
-}
-.compat-hint#hint-not-compat {
-	color: #804040;
-}
-END
-		, TRUE);
-		// render port type editor form
-		echo '<form method=GET>';
-		echo '<input type=hidden name="module" value="popup">';
-		echo '<input type=hidden name="helper" value="portlist">';
-		echo '<input type=hidden name="port" value="' . $port_info['id'] . '">';
-		echo '<input type=hidden name="remote_port" value="' . $remote_port_info['id'] . '">';
-		echo '<input type=hidden name="cable" value="' . htmlspecialchars ($_REQUEST['cable'], ENT_QUOTES) . '">';
-		echo '<p>The ports you have selected are not compatible. Please select a compatible transceiver pair.';
-		echo '<p>';
-		echo formatPort ($port_info) . ' ';
-		if ($port_info['iif_id'] == 1)
-		{
-			echo formatPortIIFOIF ($port_info);
-			echo '<input type=hidden name="port_type" value="' . $port_info['oif_id'] . '">';
-		}
-		else
-		{
-			echo '<label>' . $port_info['iif_name'] . ' ';
-			printSelect (getExistingPortTypeOptions ($port_info['id']), array ('class' => 'porttype', 'name' => 'port_type'), $type_local);
-			echo '</label>';
+			, true);
+			return;
 		}
-		echo ' &mdash; ';
-		if ($remote_port_info['iif_id'] == 1)
-		{
-			echo formatPortIIFOIF ($remote_port_info);
-			echo '<input type=hidden name="remote_port_type" value="' . $remote_port_info['oif_id'] . '">';
-		}
-		else
-		{
-			echo '<label>' . $remote_port_info['iif_name'] . ' ';
-			printSelect (getExistingPortTypeOptions ($remote_port_info['id']), array ('class' => 'porttype', 'name' => 'remote_port_type'), $type_remote);
-			echo '</label>';
-		}
-		echo ' ' . formatPort ($remote_port_info);
-		echo '<p class="compat-hint" id="hint-not-compat">&#10005; Not compatible port types</p>';
-		echo '<p class="compat-hint" id="hint-compat">&#10004; Compatible port types</p>';
-		echo '<p><input type=submit name="do_link" value="Link">';
+	}catch( Exception $e ){
+		$dbxlink->rollBack();
+		throw $e;
 	}
+	return renderPopupPortSelector();
 }
 
 function renderPopupPortSelector()
@@ -353,7 +347,8 @@ function renderPopupPortSelector()
 	assertUIntArg ('port');
 	$port_id = $_REQUEST['port'];
 	$port_info = getPortInfo ($port_id);
-	$in_rack = isset ($_REQUEST['in_rack']);
+
+	$in_rack = $_REQUEST['in_rack'] != 'off';;
 
 	// fill port filter structure
 	$filter = array
@@ -379,11 +374,34 @@ function renderPopupPortSelector()
 		! empty ($filter['objects']) ||
 		! empty ($filter['ports'])
 	)
-		$spare_ports = findSparePorts ($port_info, $filter);
+	$ports_with_current = $ports = getSparePorts($port_info, $filter)->fetchAll();
+	if( $port_info['linked'] ){
+		$link = usePreparedSelectBlade('SELECT * FROM Link WHERE porta = ? OR portb = ?', array($port_id, $port_id))->fetch(PDO::FETCH_ASSOC);
+		
+		$current = getPortInfo( ($link['porta'] == $port_id) ? $link['portb'] : $link['porta'] );
+		$port_info['cableid'] = $current['cableid'] = $link['cable'];
+		$port_info['cable_dict_key'] = $current['cable_dict_key'] = $link['cable_dict_key'];
+		$port_info['remote_id'] = $current['id'];
 
+		$ports_with_current[] = $current;
+	}
+	addJS('js/link_port_form.js');
+	addJS(getLinkPortJavascript($port_info, $ports_with_current),true);
+	addCSS (<<<'END'
+.reserved {
+	text-decoration: line-through;
+}
+.compatible {
+	background: #afa;
+}
+.incompatible {
+	background: #faa;
+}
+END
+		, TRUE);
 	// display search form
 	echo 'Link ' . formatPort ($port_info) . ' to...';
-	echo '<form method=GET>';
+	echo '<form method="POST">';
 	startPortlet ('Port list filter');
 	echo '<input type=hidden name="module" value="popup">';
 	echo '<input type=hidden name="helper" value="portlist">';
@@ -391,23 +409,59 @@ function renderPopupPortSelector()
 	echo '<table align="center" valign="bottom"><tr>';
 	echo '<td class="tdleft"><label>Object name:<br><input type=text size=8 name="filter-obj" value="' . htmlspecialchars ($filter['objects'], ENT_QUOTES) . '"></label></td>';
 	echo '<td class="tdleft"><label>Port name:<br><input type=text size=6 name="filter-port" value="' . htmlspecialchars ($filter['ports'], ENT_QUOTES) . '"></label></td>';
-	echo '<td class="tdleft" valign="bottom"><label><input type=checkbox name="in_rack"' . ($in_rack ? ' checked' : '') . '>Nearest racks</label></td>';
+	echo '</tr><tr>';
+	echo '<td class="tdleft" valign="bottom"><input type="hidden" name="in_rack" value="off" /><label><input type=checkbox name="in_rack"' . ($in_rack ? ' checked' : '') . '>Nearest racks</label></td>';
 	echo '<td valign="bottom"><input type=submit value="show ports"></td>';
 	echo '</tr></table>';
 	finishPortlet();
 
 	// display results
-	startPortlet ('Compatible spare ports');
-	if (empty ($spare_ports))
-		echo '(nothing found)';
-	else
-	{
-		echo getSelect ($spare_ports, array ('name' => 'remote_port', 'size' => getConfigVar ('MAXSELSIZE')), NULL, FALSE);
-		echo "<p>Cable ID: <input type=text id=cable name=cable>";
-		echo "<p><input type='submit' value='Link' name='do_link'>";
+	startPortlet ('Link port');
+	if( $ports_with_current ){
+		echo '<div style="float: left;width:40%">';
+		echo '<select name="remote_port" id="remote_port" size="',getConfigVar ('MAXSELSIZE'),'" style="width:100%">';
+		if( $current ){
+			echo '<optgroup label="current">';
+			echo '<optgroup label="&nbsp; ', htmlspecialchars($current['object_name']), '">';
+			echo '<option value="',$current['id'],'" selected="selected">', htmlspecialchars($current['name'] ),'</option>';
+			echo '</optgroup>';
+		}
+		echo '<optgroup label="search result">';
+		if( !$ports ){
+			echo '<option disabled="disabled" value="">nothing found</option>';
+		}else{
+			$ports_by_object = array();
+			foreach( $ports as $port ){
+				if( !isset($ports_by_object[$port['object_name']]) ) $ports_by_object[$port['object_name']] = array();
+				$ports_by_object[$port['object_name']][] = $port;
+			}
+			ksort( $ports_by_object );
+			foreach( $ports_by_object as $name => $object_ports ){
+				echo '<optgroup label="&nbsp; ', htmlspecialchars($name), '">';
+				foreach( sortPortList($object_ports, true) as $port ){
+					echo '<option value="', $port['id'],'"',($port['reservation_comment'] ? ' class="reserved"':''),'>', htmlspecialchars($port['name']), '</option>';
+				}
+				echo '</optgroup>';
+			}
+		}
+		echo '</optgroup>';
+		echo '</select>';
+		echo '</div>';
+		echo '<div style="margin-left: 41%">';
+		echo '<p style="display:none">This port is reserved: <span id="reservation"></span></p>';
+		echo '<p style="display:none"><label for="cable_type">Local Transceiver</label></br><select name="local_oif" id="local_oif" style="width:100%"></select></p>';
+		echo '<p style="display:none"><label for="remote_oif">Remote Transceiver</label></br><select name="remote_oif" id="remote_oif" style="width:100%"></select></p>';
+		echo '<p style="display:none"><label for="cable_type">Cable type</label></br><select name="cable_type" id="cable_type" style="width:100%"></select></p>';
+		echo '<p><label for="cable">Cable ID: </label><br /><input type="text" id="cable" name="cable" style="width:100%" value="',htmlspecialchars($port_info['cableid']) ,'"/></p>';
+		echo '<p><input type="submit" value="Link" name="do_link" id="submit"></p>';
+		echo '<br style="clear:both" /></div>';
+	}else{
+		echo 'nothing found';
 	}
+
 	finishPortlet();
 	echo '</form>';
+	echo '<br style="clear:both" />';
 }
 
 function renderPopupIPv4Selector()
diff --git a/wwwroot/js/link_port_form.js b/wwwroot/js/link_port_form.js
new file mode 100644
index 0000000..0cf45a2
--- /dev/null
+++ b/wwwroot/js/link_port_form.js
@@ -0,0 +1,147 @@
+function initializeLinkportForm( PORT, PORTS, INNER_OUTER_COMPATIBILITY, OUTER_COMPATIBILITY, CABLES, OIF ){
+	var $remote_port = $('#remote_port'),$remote_oif = $('#remote_oif'),$local_oif = $('#local_oif'),$cable_type = $('#cable_type'), $submit = $('#submit'), $reservation = $('#reservation');
+	var compatClass = function(a,b){
+		if( !a || !b ){
+			return '';
+		}else if( !OUTER_COMPATIBILITY[a] || !OUTER_COMPATIBILITY[a][b]  ){
+			return 'incompatible';
+		}else{
+			return 'compatible';
+		}
+	}
+	var updateSelects = function(){
+		var rp = $remote_port.val(), ro = $remote_oif.val(), lo = $local_oif.val(), ct = $cable_type.val();
+		if( ro ) ro = parseInt(ro);
+		if( !lo ){
+			lo = PORT.oif_id;
+		}else{
+			lo = parseInt(lo);
+		}
+		if( !rp ){
+			rp = PORT.remote_id;
+		}
+		if( !ct ){
+			ct = PORT.cable_dict_key;
+		}
+		//remote_oif
+		$remote_oif.empty();
+		var remote_port;
+		if( rp ){
+			remote_port =	$.grep(PORTS,function(p){ return p.id == rp })[0];
+			if( !ro || remote_port.iif_id == 1 || INNER_OUTER_COMPATIBILITY[ remote_port.iif_id ].indexOf(ro) == -1  ){
+				ro = remote_port.oif_id;
+			}
+			if( remote_port.reservation == "" ){
+				$reservation.closest('p').hide();
+			}else{
+				$reservation.text( remote_port.reservation );
+				$reservation.closest('p').show();	
+			}
+			if( remote_port.iif_id != 1 ){
+				$remote_oif.closest('p').show();	
+				$remote_oif.attr('disabled',false);
+			}else{
+				$remote_oif.closest('p').hide();
+				$remote_oif.attr('disabled',true);
+			}
+			var rps = INNER_OUTER_COMPATIBILITY[ remote_port.iif_id ];
+			rps.sort(function(a,b){
+				return OIF[a].localeCompare(OIF[b]);
+			}).forEach( function(oif){
+				$('<option>').val(oif).text( OIF[oif] ).addClass( compatClass( oif, lo )  ).appendTo($remote_oif);
+			});
+			$remote_oif.val(ro);
+		}else{
+			$remote_oif.closest('p').hide();	
+			$remote_oif.attr('disabled',true);
+		}
+		//local_oif
+		$local_oif.empty();
+		if( PORT.iif_id != 1 ){
+			$local_oif.closest('p').show();
+			$local_oif.attr('disabled',false);
+		}else{
+			$local_oif.closest('p').hide();
+			$local_oif.attr('disabled',true);	
+		}
+		var lps = INNER_OUTER_COMPATIBILITY[ PORT.iif_id ];
+		lps.sort(function(a,b){
+			return OIF[a].localeCompare(OIF[b]);
+		}).forEach( function(oif){
+			$('<option>').val(oif).text( OIF[oif] ).addClass( compatClass( oif, ro ) ).appendTo($local_oif);
+		});
+		$local_oif.val(lo);
+		if( rp && lo && ro ){
+			// cables and compat!
+			var comb = OUTER_COMPATIBILITY[ ro ] ? OUTER_COMPATIBILITY[ ro ][ lo ] : null;
+			if( !comb ){
+				$submit.attr('disabled',true);
+				$submit.val('Incompatible transceiver');
+				$cable_type.closest('p').hide();
+			}else{
+				if( comb.cable_chapter_id ){
+					$cable_type.empty();
+					var cables = CABLES[comb.cable_chapter_id];
+					if( cables ){
+						var other;
+						$.each(cables,function(i,group){
+							if( group.name == 'other' ){
+								other = group;
+								return ;
+							}
+							var g = $('<optgroup>').attr('label',group.name).appendTo($cable_type);
+							$.each(group.values, function(j, opt){
+								$('<option>').val(opt.key).text(opt.value).appendTo(g);
+							});
+						});
+						if( other ){
+							$.each(other.values, function(j, opt){
+								$('<option>').val(opt.key).text(opt.value).appendTo($cable_type);
+							});
+						}
+					}
+				 $('<option>').val('').text('').appendTo($cable_type);	
+					$cable_type.val(ct);
+					$cable_type.closest('p').show();
+				}else{
+					// compatible but no cables
+					$cable_type.closest('p').hide();
+				}
+				$submit.attr('disabled',false);
+				$submit.val('Link');
+			}
+		}else{
+			if( !rp ){
+				$submit.attr('disabled',true);
+				$submit.val('Select a port');	
+			}
+			$cable_type.closest('p').hide();
+		}
+	}
+	updateSelects();
+	$remote_port.change(updateSelects);
+	var changeTransceiver = function( $a, $b ){
+		var av = $a.val();
+		var bv = $b.val();
+		if( !OUTER_COMPATIBILITY[av] ){
+			return ;
+		}
+		if(	OUTER_COMPATIBILITY[av][bv] ){
+			return;
+		}
+		$b.children('option').each(function(i,o ){
+			if( OUTER_COMPATIBILITY[ av ][ $(o).val() ] ){
+				$b.val( $(o).val() );
+				return false;
+			}
+		});
+	}
+	$local_oif.change(function(){
+		changeTransceiver($local_oif,$remote_oif);
+		updateSelects();
+	});
+	$remote_oif.change(function(){
+		changeTransceiver($remote_oif,$local_oif);
+		updateSelects();
+	}); 
+}

Relationships

child of 333 assignedadoom42 Add sql to /install/init.structure.sql to create PatchPanelMap table 

Activities

user292

2012-04-19 10:55

  ~0000657

Hi,

I made good progress. The biggest changes happened to the link-port form. This was necessary, to keep the numbers of additional clicks low. Furthermore you can now select transceivers without submitting the form.

Cheers!
Hannes
infrastation

infrastation

2012-04-22 08:10

administrator   ~0000661

Hannes, I will review the patch and update this issue.

user292

2012-04-23 08:16

  ~0000665

Nice! When you are done, I'll backport this to 0.19.13.

user292

2012-07-02 13:33

  ~0000699

Hi,

I just backported the feature on top of the 0.19.13 release.

Cheers!

user292

2012-07-03 07:12

  ~0000700

Sorry, I forgot to check "nearest rack" by default. Corrected this.
infrastation

infrastation

2013-11-27 13:05

administrator   ~0001989

Aaron, these changes would conflict with the patch panels code but there are good points in this work that should be considered. I have reassigned this issue to you.
adoom42

adoom42

2015-06-11 03:59

administrator   ~0002853

Re-assigning to infrastation since he added the "Patch cables" feature.

Issue History

Date Modified Username Field Change
2012-04-12 11:46 user292 New Issue
2012-04-19 10:40 user292 File Added: track_cable_type.patch
2012-04-19 10:51 user292 File Added: track_cable_type.sql
2012-04-19 10:55 user292 Note Added: 0000657
2012-04-22 08:10 infrastation Note Added: 0000661
2012-04-22 08:10 infrastation Status new => acknowledged
2012-04-23 08:16 user292 Note Added: 0000665
2012-07-02 13:32 user292 File Added: track_cable_type_0.19.13.diff
2012-07-02 13:33 user292 Note Added: 0000699
2012-07-03 07:12 user292 File Added: track_cable_type_0.19.13.correct.diff
2012-07-03 07:12 user292 Note Added: 0000700
2012-08-23 14:34 infrastation Assigned To => infrastation
2012-08-23 14:34 infrastation Status acknowledged => assigned
2013-11-27 13:03 infrastation Relationship added child of 333
2013-11-27 13:03 infrastation Assigned To infrastation => adoom42
2013-11-27 13:05 infrastation Note Added: 0001989
2015-06-11 03:59 adoom42 Note Added: 0002853
2015-06-11 03:59 adoom42 Assigned To adoom42 => infrastation