justify page title handling
[racktables-incomplete-works] / wwwroot / inc / interface-lib.php
index aaa61b31094e970fd605a7c278e7af9634354dc8..a8bd4089ca526e2976cb67c0f721abdd750a5b24 100644 (file)
@@ -1,13 +1,11 @@
 <?php
 
-define ('TAGNAME_REGEXP', '/^[\p{L}0-9]([. _~-]?[\p{L}0-9])*$/u');
-define ('AUTOTAGNAME_REGEXP', '/^\$[\p{L}0-9]([. _~-]?[\p{L}0-9])*$/u');
+# This file is a part of RackTables, a datacenter and server room management
+# framework. See accompanying file "COPYING" for the full copyright and
+# licensing information.
 
 // Let's have it here, so extensions can add their own images.
 $image = array();
-$image['logo']['path'] = 'pix/defaultlogo.png';
-$image['logo']['width'] = 210;
-$image['logo']['height'] = 40;
 $image['rackspace']['path'] = 'pix/racks.png';
 $image['rackspace']['width'] = 218;
 $image['rackspace']['height'] = 200;
@@ -116,6 +114,9 @@ $image['COMMIT gray']['height'] = 32;
 $image['RECALC']['path'] = 'pix/tango-view-refresh-32x32.png';
 $image['RECALC']['width'] = 32;
 $image['RECALC']['height'] = 32;
+$image['recalc']['path'] = 'pix/tango-view-refresh-16x16.png';
+$image['recalc']['width'] = 16;
+$image['recalc']['height'] = 16;
 $image['clear']['path'] = 'pix/tango-edit-clear.png';
 $image['clear']['width'] = 16;
 $image['clear']['height'] = 16;
@@ -173,6 +174,9 @@ $image['object']['height'] = 16;
 $image['OBJECT']['path'] = 'pix/bracket-32x32.png';
 $image['OBJECT']['width'] = 32;
 $image['OBJECT']['height'] = 32;
+$image['LOCATION']['path'] = 'pix/tango-internet-32x32.png';
+$image['LOCATION']['width'] = 32;
+$image['LOCATION']['height'] = 32;
 $image['attach']['path'] = 'pix/tango-mail-attachment-16x16.png';
 $image['attach']['width'] = 16;
 $image['attach']['height'] = 16;
@@ -257,12 +261,28 @@ $image['DQUEUE failed']['height'] = 32;
 $image['DQUEUE disabled']['path'] = 'pix/tango-emblem-readonly-32x32.png';
 $image['DQUEUE disabled']['width'] = 32;
 $image['DQUEUE disabled']['height'] = 32;
+$image['copy']['path'] = 'pix/tango-edit-copy-16x16.png';
+$image['copy']['width'] = 16;
+$image['copy']['height'] = 16;
 $image['COPY']['path'] = 'pix/tango-edit-copy-32x32.png';
 $image['COPY']['width'] = 32;
 $image['COPY']['height'] = 32;
 $image['html']['path'] = 'pix/tango-text-html.png';
 $image['html']['width'] = 16;
 $image['html']['height'] = 16;
+$image['pencil']['path'] = 'pix/pencil-icon.png';
+$image['pencil']['width'] = 12;
+$image['pencil']['height'] = 12;
+
+$page_by_realm = array();
+$page_by_realm['object'] = 'depot';
+$page_by_realm['rack'] = 'rackspace';
+$page_by_realm['ipv4net'] = 'ipv4space';
+$page_by_realm['ipv6net'] = 'ipv6space';
+$page_by_realm['ipv4vs'] = 'ipv4slb';
+$page_by_realm['ipv4rspool'] = 'ipv4slb';
+$page_by_realm['file'] = 'files';
+$page_by_realm['user'] = 'userlist';
 
 function printSelect ($optionList, $select_attrs = array(), $selected_id = NULL)
 {
@@ -280,7 +300,8 @@ function getSelect ($optionList, $select_attrs = array(), $selected_id = NULL, $
                return '(none)';
        if (count ($optionList) == 1 && $treat_single_special)
        {
-               foreach ($optionList as $key => $value) { break; }
+               foreach ($optionList as $key => $value)
+                       break;
                return "<input type=hidden name=${select_attrs['name']} id=${select_attrs['name']} value=${key}>" . $value;
        }
        if (!array_key_exists ('id', $select_attrs))
@@ -370,8 +391,8 @@ function getOptionTree ($tree_name, $tree_options, $tree_config = array())
                # way, hence a structure transform is required.
                foreach ($tree_options as $key => $value)
                        $tmp[] = is_array ($value) ?
-                               "\"${key}\": " . $self ($value) :
-                               "\"${value}\": ${key}";
+                               '"' . str_replace ('"', '\"', $key) . '": ' . $self ($value) :
+                               '"' . str_replace ('"', '\"', $value) . '": "' . str_replace ('"', '\"', $key) . '"';
                return '{' . implode (', ', $tmp) . "}\n";
        }
 
@@ -427,11 +448,6 @@ function getImageHREF ($tag, $title = '', $do_input = FALSE, $tabindex = 0)
                        ">";
 }
 
-function dos2unix ($text)
-{
-       return str_replace ("\r\n", "\n", $text);
-}
-
 function escapeString ($value, $do_db_escape = FALSE)
 {
        $ret = htmlspecialchars ($value, ENT_QUOTES, 'UTF-8');
@@ -454,8 +470,8 @@ function transformRequestData()
        // Escape any globals before we ever try to use them, but keep a copy of originals.
        $sic = array();
        // walk through merged GET and POST instead of REQUEST array because it
-       // can contain cookies with data which could not be decoded from UTF-8
-       foreach (array_merge($_GET, $_POST) as $key => $value)
+       // can contain cookies with data that could not be decoded from UTF-8
+       foreach (($_POST + $_GET) as $key => $value)
        {
                if (is_array ($value))
                        $_REQUEST[$key] = $value;
@@ -489,7 +505,7 @@ function addJS ($data, $inline = FALSE, $group = 'default')
 {
        static $javascript = array();
        static $seen_filenames = array();
-       
+
        if (! isset ($data))
        {
                ksort ($javascript);
@@ -539,7 +555,7 @@ function addCSS ($data, $inline = FALSE)
 {
        static $styles = array();
        static $seen_filenames = array();
-       
+
        if (! isset ($data))
                return $styles;
        if ($inline)
@@ -559,6 +575,144 @@ function addCSS ($data, $inline = FALSE)
        }
 }
 
+function getRenderedIPNetCapacity ($range)
+{
+       switch (strlen ($range['ip_bin']))
+       {
+               case 4:  return getRenderedIPv4NetCapacity ($range);
+               case 16: return getRenderedIPv6NetCapacity ($range);
+               default: throw new InvalidArgException ('range["ip_bin"]', $range['ip_bin'], "Invalid binary IP");
+       }
+}
+
+function getRenderedIPv4NetCapacity ($range)
+{
+       $class = 'net-usage';
+       if (isset ($range['addrc']))
+       {
+               // full mode
+               // $a is "aquamarine zone", $b is "gray zone"
+               $total = ip4_range_size ($range);
+
+               // compute $a_total: own range size, without subranges
+               if (! isset ($range['kidc']) or $range['kidc'] == 0)
+                       $a_total = $total;
+               else
+               {
+                       $a_total = 0;
+                       foreach ($range['spare_ranges'] as $mask => $spare_list)
+                               $a_total = bcadd ($a_total, bcmul (count ($spare_list), ip4_mask_size ($mask)), 0);
+               }
+               $a_used = $range['own_addrc'];
+               $b_total = bcsub ($total, $a_total, 0);
+               $b_used = $range['addrc'] - $a_used;
+
+               // generate link to progress bar image
+               $width = 100;
+               if ($total != 0)
+               {
+                       $px_a = round (bcdiv ($a_total, $total, 4) * $width);
+                       $px1 = round (bcdiv ($a_used, $total, 4) * $width);
+                       $px2 = $px_a - $px1;
+                       $px3 = round (bcdiv ($b_used, $total, 4) * $width);
+                       if ($px3 + $px1 + $px2 > $width)
+                               $px3 = $width - $px1 - $px2;
+               }
+               else
+                       $px1 = $px2 = $px3 = 0;
+
+               $title_items = array();
+               $title2_items = array();
+               if ($a_total != 0)
+               {
+                       $title_items[] = "$a_used / $a_total";
+                       $title2_items[] = sprintf ("%d%% used", bcdiv ($a_used, $a_total, 4) * 100);
+               }
+               if ($b_total != 0)
+               {
+                       $title_items[] = ($b_used ? "$b_used / " : "") . $b_total;
+                       $title2_items[] = sprintf ("%d%% sub-allocated", bcdiv ($b_total, $total, 4) * 100);
+               }
+               $title = implode (', ', $title_items);
+               $title2 = implode (', ', $title2_items);
+               $text = "<img width='$width' height=10 border=0 title='$title2' src='?module=progressbar4&px1=$px1&px2=$px2&px3=$px3'>" .
+                       " <small class='title'>$title</small>";
+       }
+       else
+       {
+               // fast mode
+               $class .= ' pending';
+               addJS ('js/net-usage.js');
+
+               $free_text = '';
+               if (isset ($range['kidc']) and $range['kidc'] > 0)
+               {
+                       $free_masks = array_keys ($range['spare_ranges']);
+                       sort ($free_masks, SORT_NUMERIC);
+                       if ($mask = array_shift ($free_masks))
+                       {
+                               $cnt = count ($range['spare_ranges'][$mask]);
+                               $free_text = ', ' . ($cnt > 1 ? "<small>${cnt}x</small>" : "") . "/$mask free";
+                       }
+               }
+               $text =  ip4_range_size ($range) . $free_text;
+       }
+
+       $div_id = $range['ip'] . '/' . $range['mask'];
+
+       return "<div class=\"$class\" id=\"$div_id\">" . $text . "</div>";
+}
+
+function getRenderedIPv6NetCapacity ($range)
+{
+       $div_id = $range['ip'] . '/' . $range['mask'];
+       $class = 'net-usage';
+       if (isset ($range['addrc']))
+               $used = $range['addrc'];
+       else
+       {
+               $used = NULL;
+               $class .= ' pending';
+               addJS ('js/net-usage.js');
+       }
+
+       static $prefixes = array
+       (
+               0 =>  '',
+               3 =>  'k',
+               6 =>  'M',
+               9 =>  'G',
+               12 => 'T',
+               15 => 'P',
+               18 => 'E',
+               21 => 'Z',
+               24 => 'Y',
+       );
+
+       if ($range['mask'] <= 64)
+       {
+               $what = 'net';
+               $preposition = 'in';
+               $range['mask'] += 64;
+       }
+       else
+       {
+               $what = 'IP';
+               $preposition = 'of';
+       }
+       $what .= (0 == $range['mask'] % 64 ? '' : 's');
+       $addrc = isset ($used) ? "$used $preposition " : '';
+
+       $dec_order = intval ((128 - $range['mask']) / 10) * 3;
+       $mult = isset ($prefixes[$dec_order]) ? $prefixes[$dec_order] : '??';
+
+       $cnt = 1 << ((128 - $range['mask']) % 10);
+       if ($cnt == 1 && $mult == '')
+               $cnt = '1';
+
+       return "<div class=\"$class\" id=\"$div_id\">" . "{$addrc}${cnt}${mult} ${what}" . "</div>";
+}
+
 // print part of HTML HEAD block
 function printPageHeaders ()
 {
@@ -583,29 +737,74 @@ function printPageHeaders ()
                                echo "<script type='text/javascript' src='?module=chrome&uri=${item['script']}'></script>\n";
 }
 
-function validTagName ($s, $allow_autotag = FALSE)
+function cmpTags ($a, $b)
+{
+       global $taglist;
+       if (isset ($a['id']) && isset ($b['id']))
+       {
+               $a_root = array_first ($taglist[$a['id']]['trace']);
+               $b_root = array_first ($taglist[$b['id']]['trace']);
+               if ($a_root < $b_root)
+                       return -1;
+               elseif ($a_root > $b_root)
+                       return 1;
+       }
+       elseif (isset ($a['id']))
+               return -1;
+       elseif (isset ($b['id']))
+               return 1;
+
+       return strcmp ($a['tag'], $b['tag']);
+}
+
+function getTagClassName ($tagid)
 {
-       if (1 == preg_match (TAGNAME_REGEXP, $s))
-               return TRUE;
-       if ($allow_autotag and 1 == preg_match (AUTOTAGNAME_REGEXP, $s))
-               return TRUE;
-       return FALSE;
+       global $taglist;
+
+       $class = '';
+       foreach ($taglist[$tagid]['trace'] as $parent)
+               $class .= 'tag-' . $parent . ' ';
+       $class .= 'tag-' . $tagid . ' etag-' . $tagid;
+
+       return $class;
 }
 
 function serializeTags ($chain, $baseurl = '')
 {
+       global $taglist;
        $tmp = array();
+       usort ($chain, 'cmpTags');
        foreach ($chain as $taginfo)
        {
+               $title = '';
+               if (isset ($taginfo['user']) and isset ($taginfo['time']))
+                       $title = htmlspecialchars ($taginfo['user'] . ', ' . formatAge ($taginfo['time']), ENT_QUOTES);
+               if (isset($taginfo['parent_id']))
+               {
+                       $parent_info = array();
+                       foreach ($taglist[$taginfo['id']]['trace'] as $tag_id)
+                               $parent_info[] = $taglist[$tag_id]['tag'];
+                       $parent_info[] = $taginfo['tag'];
+                       if (strlen ($title))
+                               $title .= "\n";
+                       $title .= implode (" \xE2\x86\x92  ", $parent_info); # right arrow
+               }
+               if (strlen ($title))
+                       $title = "title='$title'";
+
+               $class = '';
+               if (isset ($taginfo['id']))
+                       $class = 'class="' . getTagClassName ($taginfo['id']) . '"';
+
+               $href = '';
                if ($baseurl == '')
-                       $tmp[] = $taginfo['tag'];
+                       $tag = 'span';
                else
                {
-                       $title = '';
-                       if (isset ($taginfo['user']) and isset ($taginfo['time']))
-                               $title = 'title="' . htmlspecialchars ($taginfo['user'] . ', ' . formatAge ($taginfo['time']), ENT_QUOTES) . '"';
-                       $tmp[] = "<a $title href='${baseurl}cft[]=${taginfo['id']}'>" . $taginfo['tag'] . "</a>";
+                       $tag = 'a';
+                       $href = "href='${baseurl}cft[]=${taginfo['id']}'";
                }
+               $tmp[] = "<$tag $href $title $class>" . $taginfo['tag'] . "</$tag>";
        }
        return implode (', ', $tmp);
 }
@@ -620,4 +819,277 @@ function finishPortlet ()
        echo "</div>\n";
 }
 
+function getPageName ($page_code)
+{
+       global $page;
+       $title = isset ($page[$page_code]['title']) ? $page[$page_code]['title'] : callHook ('dynamic_title_decoder', $page_code);
+       return niftyString (is_array ($title) ? $title['name'] : $title);
+}
+
+function printTagTRs ($cell, $baseurl = '')
+{
+       if (getConfigVar ('SHOW_EXPLICIT_TAGS') == 'yes' and count ($cell['etags']))
+       {
+               echo "<tr><th width='50%' class=tagchain>Explicit tags:</th><td class=tagchain>";
+               echo serializeTags ($cell['etags'], $baseurl) . "</td></tr>\n";
+       }
+       if (getConfigVar ('SHOW_IMPLICIT_TAGS') == 'yes' and count ($cell['itags']))
+       {
+               echo "<tr><th width='50%' class=tagchain>Implicit tags:</th><td class=tagchain>";
+               echo serializeTags ($cell['itags'], $baseurl) . "</td></tr>\n";
+       }
+       if (getConfigVar ('SHOW_AUTOMATIC_TAGS') == 'yes' and count ($cell['atags']))
+       {
+               echo "<tr><th width='50%' class=tagchain>Automatic tags:</th><td class=tagchain>";
+               echo serializeTags ($cell['atags']) . "</td></tr>\n";
+       }
+}
+
+// stub function to override it by chain-connected hooks
+function modifyEntitySummary ($cell, $summary)
+{
+       return $summary;
+}
+
+// renders 'summary' portlet, which persist on default tab of every realm page.
+// $values is a tricky array.
+// if its value is a string, it is treated as right td inner html, and the key is treated as left th text, colon appends there automatically.
+// 'tags' key has a special meaning: instead of value, the result of printTagTRs call is appended to output
+// if the value is a single-element array, its value rendered as-is instead of <tr> tag and all its contents.
+// if the value is an array, its first 2 items are treated as left and right contents of row, no colon is appended. Used to enable non-unique titles
+function renderEntitySummary ($cell, $title, $values = array())
+{
+       global $page_by_realm;
+       // allow plugins to override summary table
+       $values = callHook ('modifyEntitySummary', $cell, $values);
+
+       startPortlet ($title);
+       echo "<table border=0 cellspacing=0 cellpadding=3 width='100%'>\n";
+       foreach ($values as $name => $value)
+       {
+               if (is_array ($value) and count ($value) == 1)
+               {
+                       $value = array_shift ($value);
+                       echo $value;
+                       continue;
+               }
+               if (is_array ($value))
+               {
+                       $name = array_shift ($value);
+                       $value = array_shift ($value);
+               }
+               elseif (! is_array ($value))
+                       $name .= ':';
+               $class = 'tdright';
+               $m = array();
+               if (preg_match('/^\{(.*?)\}(.*)/', $name, $m))
+               {
+                       $class .= ' ' . $m[1];
+                       $name = $m[2];
+               }
+               if ($name == 'tags:')
+               {
+                       $baseurl = '';
+                       if (isset ($page_by_realm[$cell['realm']]))
+                               $baseurl =  makeHref(array('page'=>$page_by_realm[$cell['realm']], 'tab'=>'default'))."&";
+                       printTagTRs ($cell, $baseurl);
+               }
+               else
+                       echo "<tr><th width='50%' class='$class'>$name</th><td class=tdleft>$value</td></tr>";
+       }
+       echo "</table>\n";
+       finishPortlet();
+}
+
+function getOpLink ($params, $title,  $img_name = '', $comment = '', $class = '')
+{
+       if (isset ($params))
+               $ret = '<a href="' . makeHrefProcess ($params) . '"';
+       else
+       {
+               $ret = '<a href="#" onclick="return false;"';
+               $class .= ' noclick';
+       }
+       if (! empty ($comment))
+               $ret .= ' title="' . htmlspecialchars ($comment, ENT_QUOTES) . '"';
+       $class = trim ($class);
+       if (! empty ($class))
+               $ret .= ' class="' . htmlspecialchars ($class, ENT_QUOTES) . '"';
+       $ret .= '>';
+       if (! empty ($img_name))
+       {
+               $ret .= getImageHREF ($img_name, $comment);
+               if (! empty ($title))
+                       $ret .= ' ';
+       }
+       if (FALSE !== strpos ($class, 'need-confirmation'))
+               addJS ('js/racktables.js');
+       $ret .= $title . '</a>';
+       return $ret;
+}
+
+function renderProgressBar ($percentage = 0, $theme = '', $inline = FALSE)
+{
+       echo getProgressBar ($percentage, $theme, $inline);
+}
+
+function getProgressBar ($percentage = 0, $theme = '', $inline = FALSE)
+{
+       $done = ((int) ($percentage * 100));
+       if (! $inline)
+               $src = "?module=progressbar&done=$done" . (empty ($theme) ? '' : "&theme=${theme}");
+       else
+       {
+               $bk_request = $_REQUEST;
+               $_REQUEST['theme'] = $theme;
+               $src = 'data:image/png;base64,' . chunk_split (base64_encode (getOutputOf ('renderProgressBarImage', $done)));
+               $_REQUEST = $bk_request;
+               header ('Content-type: text/html');
+       }
+       $ret = "<img width=100 height=10 border=0 title='${done}%' src='$src'>";
+       return $ret;
+}
+
+function renderNetVLAN ($cell)
+{
+       if (empty ($cell['8021q']))
+               return;
+       $links = array();
+       foreach ($cell['8021q'] as $vi)
+               $links[] = mkA ($vi['vlan_id'], 'vlan', "${vi['domain_id']}-${vi['vlan_id']}");
+       $noun = count ($cell['8021q']) > 1 ? 'VLANs' : 'VLAN';
+       echo "<div class='vlan'><strong><small>${noun}</small> " . implode (', ', $links) . '</strong></div>';
+}
+
+function includeJQueryUI ($do_css = TRUE)
+{
+       addJS ('js/jquery-ui-1.8.21.min.js');
+       if ($do_css)
+               addCSS ('css/jquery-ui-1.8.22.redmond.css');
+}
+
+function getRenderedIPPortPair ($ip, $port = NULL)
+{
+       return "<a href=\"" .
+               makeHref (array ('page' => 'ipaddress',  'tab'=>'default', 'ip' => $ip)) .
+               "\">" . $ip . "</a>" .
+               (isset ($port) ? ":" . $port : "");
+}
+
+// Print common operation form prologue, include bypass argument, if
+// appropriate, and some extra hidden inputs, if requested.
+// Use special encoding for upload forms
+function printOpFormIntro ($opname, $extra = array(), $upload = FALSE)
+{
+       global $pageno, $tabno, $page;
+
+       echo "<form method=post id=${opname} name=${opname} action='?module=redirect&page=${pageno}&tab=${tabno}&op=${opname}'";
+       if ($upload)
+               echo " enctype='multipart/form-data'";
+       echo ">";
+       fillBypassValues ($pageno, $extra);
+       foreach ($extra as $inputname => $inputvalue)
+               printf ('<input type=hidden name="%s" value="%s">', htmlspecialchars ($inputname, ENT_QUOTES), htmlspecialchars ($inputvalue, ENT_QUOTES));
+}
+
+
+// Display hrefs for all of a file's parents. If scissors are requested,
+// prepend cutting button to each of them.
+function serializeFileLinks ($links, $scissors = FALSE)
+{
+       $comma = '';
+       $ret = '';
+       foreach ($links as $link_id => $li)
+       {
+               $cell = spotEntity ($li['entity_type'], $li['entity_id']);
+               $ret .= $comma;
+               if ($scissors)
+                       $ret .= getOpLink (array('op'=>'unlinkFile', 'link_id'=>$link_id), '', 'cut', 'Unlink file') . ' ';
+               $ret .= mkCellA ($cell);
+               $comma = '<br>';
+       }
+       return $ret;
+}
+
+// This is a dual-purpose formating function:
+// 1. Replace empty strings with nbsp.
+// 2. Cut strings that are too long: append "cut here" indicator and provide a mouse hint.
+function niftyString ($string, $maxlen = 30, $usetags = TRUE)
+{
+       $cutind = '&hellip;'; // length is 1
+       if (!mb_strlen ($string))
+               return '&nbsp;';
+       // a tab counts for a space
+       $string = preg_replace ("/\t/", ' ', $string);
+       if (!$maxlen or mb_strlen ($string) <= $maxlen)
+               return htmlspecialchars ($string, ENT_QUOTES, 'UTF-8');
+       return
+               ($usetags ? ("<span title='" . htmlspecialchars ($string, ENT_QUOTES, 'UTF-8') . "'>") : '') .
+               str_replace (' ', '&nbsp;', htmlspecialchars (mb_substr ($string, 0, $maxlen - 1), ENT_QUOTES, 'UTF-8')) .
+               $cutind .
+               ($usetags ? '</span>' : '');
+}
+
+function printTagsPicker ($preselect=NULL)
+{
+       printTagsPickerInput ();
+       printTagsPickerUl ($preselect);
+       enableTagsPicker ();
+}
+
+function printTagsPickerInput ($input_name="taglist")
+{
+       # use data-attribute as identifier for tagit
+       echo "<input type='text' data-tagit-valuename='" . $input_name . "' data-tagit='yes' placeholder='new tags here...' class='ui-autocomplete-input' autocomplete='off' role='textbox' aria-autocomplete='list' aria-haspopup='true'>";
+       echo "<span title='show tag tree' class='icon-folder-open tagit_input_" . $input_name . "'></span>";
+}
+
+function printTagsPickerUl ($preselect=NULL, $input_name="taglist")
+{
+       global $target_given_tags;
+       if ($preselect === NULL)
+               $preselect = $target_given_tags;
+       foreach ($preselect as $key => $value) # readable time format
+               $preselect[$key]['time_parsed'] = formatAge ($value['time']);
+       usort ($preselect, 'cmpTags');
+       $preselect_hidden = "";
+       foreach ($preselect as $value){
+               $preselect_hidden .= "<input type=hidden name=" . $input_name . "[] value=" . $value['id'] . ">";
+       }
+       echo $preselect_hidden; # print preselected tags id that used in case javascript problems
+       echo "<ul data-tagit='yes' data-tagit-valuename='" . $input_name . "' data-tagit-preselect='" . json_encode($preselect) . "' class='tagit-vertical'></ul>";
+}
+
+function enableTagsPicker ()
+{
+       global $taglist;
+       static $taglist_inserted;
+       includeJQueryUI ();
+       addCSS ('css/tagit.css');
+       addJS ('js/tag-it.js');
+       addJS ('js/tag-it-local.js');
+       if (! $taglist_inserted)
+       {
+               $taglist_filtered = array();
+               foreach ($taglist as $key => $taginfo) # remove unused fields
+                       $taglist_filtered[$key] = array_sub ($taginfo, array("tag", "is_assignable", "trace"));
+               addJS ('var taglist = ' . json_encode ($taglist_filtered) . ';', TRUE);
+               $taglist_inserted = TRUE;
+       }
+}
+
+function makeIPAllocLink ($ip_bin, $alloc, $display_ifname = FALSE)
+{
+       $object_name = ! isset ($object_name) || ! strlen ($object_name) ?
+               formatEntityName (spotEntity ('object', $alloc['object_id'])) :
+               $alloc['object_name'];
+       $title = $display_ifname ?
+               '' :
+               "{$alloc['name']} @ {$object_name}";
+       return
+               '<a href="' . makeHref (array ('page' => 'object', 'tab' => 'default', 'object_id' => $alloc['object_id'], 'hl_ip' => ip_format ($ip_bin))) . '"' .
+               ' title="' . htmlspecialchars ($title, ENT_QUOTES) . '"' .
+               ">" . ($display_ifname ? $alloc['name'] . '@' : '') . $object_name . "</a>";
+}
+
 ?>