[freeside-commits] branch FREESIDE_2_3_BRANCH updated. b3f8d200be3833b4f60ceae973f2e76d57e0b3cb

Mark Wells mark at 420.am
Fri Nov 30 17:40:00 PST 2012


The branch, FREESIDE_2_3_BRANCH has been updated
       via  b3f8d200be3833b4f60ceae973f2e76d57e0b3cb (commit)
      from  88ee94c8cbe28332a40c3d2ca3f84f3e14867a5b (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit b3f8d200be3833b4f60ceae973f2e76d57e0b3cb
Author: Mark Wells <mark at freeside.biz>
Date:   Fri Nov 30 17:38:45 2012 -0800

    broadband_snmp export: better MIB selection, #16588

diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 168fdda..5bd047d 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -56,6 +56,7 @@ if ( -e $addl_handler_use_file ) {
   #use CGI::Carp qw(fatalsToBrowser);
   use CGI::Cookie;
   use List::Util qw( max min sum );
+  use Scalar::Util qw( blessed );
   use Data::Dumper;
   use Date::Format;
   use Time::Local;
diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm
index 45773e0..6768f12 100644
--- a/FS/FS/part_export.pm
+++ b/FS/FS/part_export.pm
@@ -417,6 +417,19 @@ sub weight {
   export_info()->{$self->exporttype}->{'weight'} || 0;
 }
 
+=item info
+
+Returns a reference to (a copy of) the export's %info hash.
+
+=cut
+
+sub info {
+  my $self = shift;
+  $self->{_info} ||= { 
+    %{ export_info()->{$self->exporttype} }
+  };
+}
+
 =back
 
 =head1 SUBROUTINES
diff --git a/FS/FS/part_export/broadband_snmp.pm b/FS/FS/part_export/broadband_snmp.pm
index cb1740e..e3a75cc 100644
--- a/FS/FS/part_export/broadband_snmp.pm
+++ b/FS/FS/part_export/broadband_snmp.pm
@@ -3,7 +3,7 @@ package FS::part_export::broadband_snmp;
 use strict;
 use vars qw(%info $DEBUG);
 use base 'FS::part_export';
-use Net::SNMP qw(:asn1 :snmp);
+use SNMP;
 use Tie::IxHash;
 
 $DEBUG = 0;
@@ -11,21 +11,21 @@ $DEBUG = 0;
 my $me = '['.__PACKAGE__.']';
 
 tie my %snmp_version, 'Tie::IxHash',
-  v1  => 'snmpv1',
-  v2c => 'snmpv2c',
-  # 3 => 'v3' not implemented
+  v1  => '1',
+  v2c => '2c',
+  # v3 unimplemented
 ;
 
-tie my %snmp_type, 'Tie::IxHash',
-  i => INTEGER,
-  u => UNSIGNED32,
-  s => OCTET_STRING,
-  n => NULL,
-  o => OBJECT_IDENTIFIER,
-  t => TIMETICKS,
-  a => IPADDRESS,
-  # others not implemented yet
-;
+#tie my %snmp_type, 'Tie::IxHash',
+#  i => INTEGER,
+#  u => UNSIGNED32,
+#  s => OCTET_STRING,
+#  n => NULL,
+#  o => OBJECT_IDENTIFIER,
+#  t => TIMETICKS,
+#  a => IPADDRESS,
+#  # others not implemented yet
+#;
 
 tie my %options, 'Tie::IxHash',
   'version' => { label=>'SNMP version', 
@@ -33,14 +33,11 @@ tie my %options, 'Tie::IxHash',
     options => [ keys %snmp_version ],
    },
   'community' => { label=>'Community', default=>'public' },
-  (
-    map { $_.'_command', 
-          { label => ucfirst($_) . ' commands',
-            type  => 'textarea',
-            default => '',
-          }
-    } qw( insert delete replace suspend unsuspend )
-  ),
+
+  'action' => { multiple=>1 },
+  'oid'    => { multiple=>1 },
+  'value'  => { multiple=>1 },
+
   'ip_addr_change_to_new' => { 
     label=>'Send IP address changes to new address',
     type=>'checkbox'
@@ -51,27 +48,13 @@ tie my %options, 'Tie::IxHash',
 %info = (
   'svc'     => 'svc_broadband',
   'desc'    => 'Send SNMP requests to the service IP address',
+  'config_element' => '/edit/elements/part_export/broadband_snmp.html',
   'options' => \%options,
   'weight'  => 10,
   'notes'   => <<'END'
 Send one or more SNMP SET requests to the IP address registered to the service.
-Enter one command per line.  Each command is a target OID, data type flag,
-and value, separated by spaces.
-The data type flag is one of the following:
-<font size="-1"><ul>
-<li><i>i</i> = INTEGER</li>
-<li><i>u</i> = UNSIGNED32</li>
-<li><i>s</i> = OCTET-STRING (as ASCII)</li>
-<li><i>a</i> = IPADDRESS</li>
-<li><i>n</i> = NULL</li></ul>
 The value may interpolate fields from svc_broadband by prefixing the field 
 name with <b>$</b>, or <b>$new_</b> and <b>$old_</b> for replace operations.
-The value may contain whitespace; quotes are not necessary.<br>
-<br>
-For example, to set the SNMPv2-MIB "sysName.0" object to the string 
-"svc_broadband" followed by the service number, use the following 
-command:<br>
-<pre>1.3.6.1.2.1.1.5.0 s svc_broadband$svcnum</pre><br>
 END
 );
 
@@ -104,19 +87,18 @@ sub export_command {
   my $self = shift;
   my ($action, $svc_new, $svc_old) = @_;
 
-  my $command_text = $self->option($action.'_command');
-  return if !length($command_text);
-
-  warn "$me parsing ${action}_command:\n" if $DEBUG;
+  my @a = split("\n", $self->option('action'));
+  my @o = split("\n", $self->option('oid'));
+  my @v = split("\n", $self->option('value'));
   my @commands;
-  foreach (split /\n/, $command_text) {
-    my ($oid, $type, $value) = split /\s/, $_, 3;
-    $oid =~ /^(\d+\.)*\d+$/ or die "invalid OID '$oid'\n";
-    my $typenum = $snmp_type{$type} or die "unknown data type '$type'\n";
-    $value = '' if !defined($value); # allow sending an empty string
+  warn "$me parsing $action commands:\n" if $DEBUG;
+  while (@a) {
+    my $oid = shift @o;
+    my $value = shift @v;
+    next unless shift(@a) eq $action; # ignore commands for other actions
     $value = $self->substitute($value, $svc_new, $svc_old);
-    warn "$me     $oid $type $value\n" if $DEBUG;
-    push @commands, $oid, $typenum, $value;
+    warn "$me     $oid :=$value\n" if $DEBUG;
+    push @commands, $oid, $value;
   }
 
   my $ip_addr = $svc_new->ip_addr;
@@ -127,13 +109,13 @@ sub export_command {
   warn "$me opening session to $ip_addr\n" if $DEBUG;
 
   my %opt = (
-    -hostname => $ip_addr,
-    -community => $self->option('community'),
-    -timeout => $self->option('timeout') || 20,
+    DestHost  => $ip_addr,
+    Community => $self->option('community'),
+    Timeout   => ($self->option('timeout') || 20) * 1000,
   );
   my $version = $self->option('version');
-  $opt{-version} = $snmp_version{$version} or die 'invalid version';
-  $opt{-varbindlist} = \@commands; # just for now
+  $opt{Version} = $snmp_version{$version} or die 'invalid version';
+  $opt{VarList} = \@commands; # for now
 
   $self->snmp_queue( $svc_new->svcnum, %opt );
 }
@@ -150,16 +132,22 @@ sub snmp_queue {
 
 sub snmp_request {
   my %opt = @_;
-  my $varbindlist = delete $opt{-varbindlist};
-  my ($session, $error) = Net::SNMP->session(%opt);
-  die "Couldn't create SNMP session: $error" if !$session;
+  my $flatvarlist = delete $opt{VarList};
+  my $session = SNMP::Session->new(%opt);
 
   warn "$me sending SET request\n" if $DEBUG;
-  my $result = $session->set_request( -varbindlist => $varbindlist );
-  $error = $session->error();
-  $session->close();
 
-  if (!defined $result) {
+  my @varlist;
+  while (@$flatvarlist) {
+    my @this = splice(@$flatvarlist, 0, 2);
+    push @varlist, [ $this[0], 0, $this[1], undef ];
+    # XXX new option to choose the IID (array index) of the object?
+  }
+
+  $session->set(\@varlist);
+  my $error = $session->{ErrorStr};
+
+  if ( $session->{ErrorNum} ) {
     die "SNMP request failed: $error\n";
   }
 }
@@ -180,4 +168,46 @@ sub substitute {
   $value;
 }
 
+sub _upgrade_exporttype {
+  eval 'use FS::Record qw(qsearch qsearchs)';
+  # change from old style with numeric oid, data type flag, and value
+  # on consecutive lines
+  foreach my $export (qsearch('part_export',
+                      { exporttype => 'broadband_snmp' } ))
+  {
+    # for the new options
+    my %new_options = (
+      'action' => [],
+      'oid'    => [],
+      'value'  => [],
+    );
+    foreach my $action (qw(insert replace delete suspend unsuspend)) {
+      my $old_option = qsearchs('part_export_option',
+                      { exportnum   => $export->exportnum,
+                        optionname  => $action.'_command' } );
+      next if !$old_option;
+      my $text = $old_option->optionvalue;
+      my @commands = split("\n", $text);
+      foreach (@commands) {
+        my ($oid, $type, $value) = split /\s/, $_, 3;
+        push @{$new_options{action}}, $action;
+        push @{$new_options{oid}},    $oid;
+        push @{$new_options{value}},   $value;
+      }
+      my $error = $old_option->delete;
+      warn "error migrating ${action}_command option: $error\n" if $error;
+    }
+    foreach (keys(%new_options)) {
+      my $new_option = FS::part_export_option->new({
+          exportnum   => $export->exportnum,
+          optionname  => $_,
+          optionvalue => join("\n", @{ $new_options{$_} })
+      });
+      my $error = $new_option->insert;
+      warn "error inserting '$_' option: $error\n" if $error;
+    }
+  } #foreach $export
+  '';
+}
+
 1;
diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi
index 8e28f4f..b51f2c9 100755
--- a/httemplate/browse/part_export.cgi
+++ b/httemplate/browse/part_export.cgi
@@ -44,14 +44,56 @@ function part_export_areyousure(href) {
       <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
         <% itable() %>
 %         my %opt = $part_export->options;
-%         foreach my $opt ( keys %opt ) { 
+%         my $defs = $part_export->info->{options};
+%         my %multiples;
+%         foreach my $opt (keys %$defs) { # is a Tie::IxHash
+%           my $group = $defs->{$opt}->{multiple};
+%           if ( $group ) {
+%             my @values = split("\n", $opt{$opt});
+%             $multiples{$group} ||= [];
+%             push @{ $multiples{$group} }, [ $opt, @values ] if @values;
+%             delete $opt{$opt};
+%           } elsif (length($opt{$opt})) { # the normal case
+%#         foreach my $opt ( keys %opt ) { 
   
             <TR>
               <TD ALIGN="right" VALIGN="top" WIDTH="33%"><% $opt %>: </TD>
               <TD ALIGN="left" WIDTH="67%"><% encode_entities($opt{$opt}) %></TD>
             </TR>
-%         } 
-  
+%             delete $opt{$opt};
+%           }
+%         }
+%         # now any that are somehow not in the options list
+%         foreach my $opt (keys %opt) {
+%           if ( length($opt{$opt}) ) {
+            <TR>
+              <TD ALIGN="right" VALIGN="top" WIDTH="33%"><% $opt %>: </TD>
+              <TD ALIGN="left" WIDTH="67%"><% encode_entities($opt{$opt}) %></TD>
+            </TR>
+%           }
+%         }
+%         # now show any multiple-option groups
+%         foreach (sort keys %multiples) {
+%           my $set = $multiples{$_};
+            <TR><TD ALIGN="center" COLSPAN=2><TABLE CLASS="grid">
+              <TR>
+%             foreach my $col (@$set) {
+                <TH><% shift @$col %></TH>
+%             }
+              </TR>
+%           while ( 1 ) {
+              <TR>
+%             my $end = 1;
+%             foreach my $col (@$set) {
+                <TD><% shift @$col %></TD>
+%               $end = 0 if @$col;
+%             }
+              </TR>
+%             last if $end;
+%           }
+            </TABLE></TD></TR>
+%         } #foreach keys %multiples
+
         </TABLE>
       </TD>
 
diff --git a/httemplate/edit/cdr_type.cgi b/httemplate/edit/cdr_type.cgi
index 5d2c662..c696106 100644
--- a/httemplate/edit/cdr_type.cgi
+++ b/httemplate/edit/cdr_type.cgi
@@ -7,11 +7,24 @@ calls and SMS messages.  Each CDR type must have a set of rates
 configured in the rate tables.
 <BR>
 <FORM METHOD="POST" ACTION="<% "${p}edit/process/cdr_type.cgi" %>">
-<% include('/elements/auto-table.html',
-  'header' => [ 'Type#', 'Name' ],
-  'fields' => [ qw( cdrtypenum cdrtypename ) ],
+<TABLE ID="AutoTable" BORDER=0 CELLSPACING=0>
+  <TR>
+    <TH>Type#</TH>
+    <TH>Name</TH>
+  </TR>
+  <TR ID="cdr_template">
+    <TD>
+      <INPUT NAME="cdrtypenum" SIZE=16 MAXLENGTH=16 ALIGN="right">
+    </TD>
+    <TD>
+      <INPUT NAME="cdrtypename" SIZE=16 MAXLENGTH=16>
+    </TD>
+  </TR>
+<&  /elements/auto-table.html,
+  'template_row' => 'cdr_template',
   'data'   => \@data,
-  ) %>
+&>
+</TABLE>
 <INPUT TYPE="submit" VALUE="Apply changes"> </FORM> <BR>
 <% include('/elements/footer.html') %>
 <%init>
@@ -20,7 +33,6 @@ die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
 my @data = (
-  map { [ $_->cdrtypenum, $_->cdrtypename ] }
   qsearch({ 
     'table' => 'cdr_type',
     'hashref' => {},
diff --git a/httemplate/edit/elements/part_export/broadband_snmp.html b/httemplate/edit/elements/part_export/broadband_snmp.html
new file mode 100644
index 0000000..4c0367c
--- /dev/null
+++ b/httemplate/edit/elements/part_export/broadband_snmp.html
@@ -0,0 +1,101 @@
+<%doc>
+</%doc>
+<& head.html, %opt &>
+<INPUT TYPE="hidden" NAME="options" VALUE="community,version,ip_addr_change_to_new,timeout">
+<& /elements/tr-select.html,
+  label   => 'SNMP version',
+  field   => 'version',
+  options => [ '', 'v1', 'v2c' ],
+  labels  => { v1 => '1', v2c => '2c' },
+  curr_value => $part_export->option('version') &>
+<& /elements/tr-input-text.html,
+  label   => 'Community',
+  field   => 'community',
+  curr_value  => $part_export->option('community'),
+&>
+<& /elements/tr-checkbox.html,
+  label   => 'Send IP address changes to new address',
+  field   => 'ip_addr_change_to_new',
+  value   => 1,
+  curr_value => $part_export->option('ip_addr_change_to_new'),
+&>
+<& /elements/tr-input-text.html,
+  label   => 'Timeout (seconds)',
+  field   => 'timeout',
+  curr_value  => $part_export->option('timeout'),
+&>
+</TABLE>
+<script type="text/javascript">
+function open_select_mib(obj) {
+  nd(1); // if there's already one open, close it
+  var rownum = obj.rownum;
+  var curr_oid = obj.value || '';
+  var url = '<%$fsurl%>/elements/select-mib-popup.html?' +
+            'callback=receive_mib;' +
+            'arg=' + rownum +
+            ';curr_value=' + curr_oid;
+  overlib(
+    OLiframeContent(url, 550, 450, '<% $popup_name %>', 0, 'auto'),
+    CAPTION, 'Select MIB object', STICKY, AUTOSTATUSCAP,
+    MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK,
+    BGCOLOR, '#333399', CGCOLOR, '#333399',
+    CLOSETEXT, 'Close'
+  );
+}
+function receive_mib(obj, rownum) {
+  //console.log(JSON.stringify(obj));
+  // we don't really need the numeric OID or any of the other properties
+  document.getElementById('oid'+rownum).value = obj.fullname;
+  document.getElementById('datatype'+rownum).value = obj.type;
+}
+</script>
+
+<table bgcolor="#cccccc" border=0 cellspacing=3>
+<TR>
+  <TH>Action</TH>
+  <TH>Object</TH>
+  <TH>Type</TH>
+  <TH>Value</TH>
+</TR>
+<TR id="mytemplate">
+  <TD>
+    <SELECT NAME="action">
+%     foreach ('', qw(insert delete replace suspend unsuspend)) {
+      <OPTION VALUE="<%$_%>"><%$_%></OPTION>
+%     }
+    </SELECT>
+  </TD>
+  <TD>
+    <INPUT NAME="oid" ID="oid" SIZE="60" onclick="open_select_mib(this)">
+  </TD>
+  <TD>
+    <INPUT TYPE="text" NAME="datatype" ID="datatype" READONLY=1>
+  </TD>
+  <TD>
+    <INPUT NAME="value" ID="value">
+  </TD>
+</TR>
+<& /elements/auto-table.html,
+  template_row  => 'mytemplate',
+  fieldorder    => ['action', 'oid', 'datatype', 'value'],
+  data          => \@data,
+&>
+<INPUT TYPE="hidden" NAME="multi_options" VALUE="action,oid,datatype,value">
+<& foot.html, %opt &>
+<%init>
+my %opt = @_;
+my $part_export = $opt{part_export} || FS::part_export->new;
+
+my @actions = split("\n", $part_export->option('action'));
+my @oids    = split("\n", $part_export->option('oid'));
+my @types   = split("\n", $part_export->option('datatype'));
+my @values  = split("\n", $part_export->option('value'));
+
+my @data;
+while (@actions or @oids or @values) {
+  my @thisrow = (shift(@actions), shift(@oids), shift(@types), shift(@values));
+  push @data, \@thisrow if grep length($_), @thisrow;
+}
+
+my $popup_name = 'popup-'.time."-$$-".rand() * 2**32;
+</%init>
diff --git a/httemplate/edit/elements/part_export/foot.html b/httemplate/edit/elements/part_export/foot.html
new file mode 100644
index 0000000..9cb8073
--- /dev/null
+++ b/httemplate/edit/elements/part_export/foot.html
@@ -0,0 +1,6 @@
+</TABLE>
+<INPUT TYPE="hidden" NAME="nodomain" VALUE="<% $opt{export_info}{nodomain} %>">
+<INPUT TYPE="submit" VALUE="<% $opt{part_export}->exportnum ? 'Apply changes' : 'Add export' %>">
+<%init>
+my %opt = @_;
+</%init>
diff --git a/httemplate/edit/elements/part_export/head.html b/httemplate/edit/elements/part_export/head.html
new file mode 100644
index 0000000..16d2a6b
--- /dev/null
+++ b/httemplate/edit/elements/part_export/head.html
@@ -0,0 +1,13 @@
+<INPUT TYPE="hidden" NAME="exporttype" VALUE="<%$layer |h%>">
+<% ntable('cccccc', 2) %>
+<TR>
+  <TD ALIGN="right" ><% emt('Description') %></TD>
+  <TD BGCOLOR="#ffffff" WIDTH="600"><% $notes %></TD>
+</TR>
+<%init>
+my %opt = @_;
+my $layer = $opt{layer};
+my $part_export = $opt{part_export};
+my $export_info = $opt{export_info};
+my $notes = $opt{notes} || $export_info->{notes};
+</%init>
diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi
index d7219b7..e152716 100644
--- a/httemplate/edit/part_export.cgi
+++ b/httemplate/edit/part_export.cgi
@@ -68,6 +68,15 @@ my $widget = new HTML::Widgets::SelectLayers(
   'html_between'    => "</TD></TR></TABLE>\n",
   'layer_callback'  => sub {
     my $layer = shift;
+    # create 'config_element' to generate the whole layer with a Mason component
+    if ( my $include = $exports->{$layer}{config_element} ) {
+      # might need to adjust the scope of  this at some point
+      return $m->scomp($include, 
+        part_export => $part_export,
+        layer       => $layer,
+        export_info => $exports->{$layer}
+      );
+    }
     my $html = qq!<INPUT TYPE="hidden" NAME="exporttype" VALUE="$layer">!.
                ntable("#cccccc",2);
 
diff --git a/httemplate/edit/process/cdr_type.cgi b/httemplate/edit/process/cdr_type.cgi
index b661de7..ba9881d 100644
--- a/httemplate/edit/process/cdr_type.cgi
+++ b/httemplate/edit/process/cdr_type.cgi
@@ -10,7 +10,6 @@ die "access denied"
     unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
 my %vars = $cgi->Vars;
-warn Dumper(\%vars)."\n";
 
 my %old = map { $_->cdrtypenum => $_ } qsearch('cdr_type', {});
 
diff --git a/httemplate/edit/process/part_export.cgi b/httemplate/edit/process/part_export.cgi
index 21150ef..8f77d52 100644
--- a/httemplate/edit/process/part_export.cgi
+++ b/httemplate/edit/process/part_export.cgi
@@ -13,15 +13,40 @@ my $exportnum = $cgi->param('exportnum');
 
 my $old = qsearchs('part_export', { 'exportnum'=>$exportnum } ) if $exportnum;
 
+my %vars = $cgi->Vars;
 #fixup options
 #warn join('-', split(',',$cgi->param('options')));
 my %options = map {
-  my @values = $cgi->param($_);
-  my $value = scalar(@values) > 1 ? join (' ', @values) : $values[0];
+  my $value = $vars{$_};
+  $value =~ s/\0/ /g; # deal with multivalued options
   $value =~ s/\r\n/\n/g; #browsers? (textarea)
   $_ => $value;
 } split(',', $cgi->param('options'));
 
+# deal with multiline options
+# %vars should never contain incomplete rows, but just in case it does, 
+# we make a list of all the row indices that contain values, and 
+# then write a line in each option for each row, even if it's empty.
+# This ensures that all values with the same row index line up.
+my %optionrows;
+foreach my $option (split(',', $cgi->param('multi_options'))) {
+  $optionrows{$option} = {};
+  my %values; # bear with me
+  for (keys %vars) {
+    /^$option(\d+)/ or next;
+    $optionrows{$option}{$1} = $vars{$option.$1};
+    $optionrows{_ALL_}{$1} = 1 if length($vars{$option.$1});
+  }
+}
+foreach my $option (split(',', $cgi->param('multi_options'))) {
+  my $value = '';
+  foreach my $row (sort keys %{$optionrows{_ALL_}}) {
+    $value .= ($optionrows{$option}{$row} || '') . "\n";
+  }
+  chomp($value);
+  $options{$option} = $value;
+}
+
 my $new = new FS::part_export ( {
   map {
     $_, scalar($cgi->param($_));
diff --git a/httemplate/edit/rate_time.cgi b/httemplate/edit/rate_time.cgi
index 7ee39ef..9e6b873 100644
--- a/httemplate/edit/rate_time.cgi
+++ b/httemplate/edit/rate_time.cgi
@@ -15,12 +15,34 @@
     <TD><INPUT TYPE="text" NAME="ratetimename" VALUE="<% $rate_time ? $rate_time->ratetimename : '' %>"></TD>
   </TR>
 </TABLE>
-<% include('/elements/auto-table.html', 
-                      'header' => [ '', 'Start','','', '','End','','' ],
-                      'fields' => [ qw(sd sh sm sa ed eh em ea) ],
-                      'select' => [ ($day, $hour, $min, $ampm) x 2 ],
-                      'data'   => \@data,
-   ) %>
+<TABLE>
+  <TR>
+    <TH COLSPAN=4 ALIGN="center">Start</TH>
+    <TH COLSPAN=4 ALIGN="center">End</TH>
+  </TR>
+  <TR id="mytemplate">
+%   for my $pre (qw(s e)) {
+%     for my $f (qw(d h m a)) { # day, hour, minute, am/pm
+        <TD>
+          <SELECT NAME="<%$pre.$f%>">
+%       my $i = 0;
+%       while ($i < @{ $choices{$f} }) {
+            <OPTION VALUE="<%$choices{$f}[$i]%>">
+%         $i++;
+            <%$choices{$f}[$i]%></OPTION>
+%         $i++;
+%       }
+          </SELECT>
+        </TD>
+%     } #$f
+%   } #$pre
+  </TR>
+<& /elements/auto-table.html, 
+    'template_row' => 'mytemplate',
+    'data'   => \@data,
+    'fieldorder' => [qw(sd sh sm sa ed eh em ea)],
+&>
+</TABLE>
 <INPUT TYPE="submit" VALUE="<% $rate_time ? 'Apply changes' : 'Add period'%>">
 </FORM>
 <BR>
@@ -42,7 +64,12 @@ my $day = [ 0 => 'Sun',
 my $hour = [ map( {$_, sprintf('%02d',$_) } 12, 1..11 )];
 my $min  = [ map( {$_, sprintf('%02d',$_) } 0,30  )];
 my $ampm = [ 0 => 'AM', 1 => 'PM' ];
-
+my %choices = (
+  'd' => $day,
+  'h' => $hour,
+  'm' => $min,
+  'a' => $ampm,
+);
 if($ratetimenum) {
   $action = 'Edit';
   $rate_time = qsearchs('rate_time', {ratetimenum => $ratetimenum})
diff --git a/httemplate/elements/auto-table.html b/httemplate/elements/auto-table.html
index 4922274..9aff94e 100644
--- a/httemplate/elements/auto-table.html
+++ b/httemplate/elements/auto-table.html
@@ -1,166 +1,180 @@
 <%doc>
-
-Example:
-<% include('/elements/auto-table.html',
-
-              ###
-              # required
-              ###
-
-              'header'        => [ '#',  'Item', 'Amount' ],
-              'fields'        => [ 'id', 'name', 'amount' ],
-
-              ###
-              # highly recommended
-              ###
-
-              'size'          => [ 4, 12, 8 ],
-              'maxl'          => [ 4, 12, 8 ],
-              'align'         => [ 'right', 'left', 'right' ],
-
-              ###
-              # optional
-              ###
-
-              'data'          => [ [ 1,  'Widget',      25 ], 
-                                   [ 12, 'Super Widget, 7  ] ],
-              #or
-              'records'       => [ qsearch('item', { } ) ],
-              # or any other array of FS::Record objects
-
-              'select'        => [ '',
-                                   [ 1 => 'option 1',
-                                     2 => 'option 2', ...
-                                   ], # options for second field
-                                   '' ],
-
-              'prefix'        => 'mytable_',
-) %>
-
-Values will be passed through as "mytable_id1", etc.
+(within a form)
+<table>
+<tr>
+  <th>Field 1</th>
+  <th>Field 2</th>
+</tr>
+<tr id="mytemplate">
+  <td><input type="text" name="field1"></td>
+  <td><select name="field2">...</td>
+  ...
+</tr>
+</table>
+<& /elements/auto-table.html,
+  table => 'mytable',
+  template_row = 'mytemplate',
+  rows => [
+            { field1 => 'foo', field2 => 'CA', ... },
+            { field1 => 'bar', field2 => 'TX', ... }, ...
+          ],
+&>
+
+  or if you prefer:
+...
+  fieldorder => [ 'field1', 'field2', ... ],
+  rows => [
+            [ 'foo', 'CA' ],
+            [ 'bar', 'TX' ],
+          ],
+
+In the process/ handler, something like:
+my @rows;
+my %vars = $cgi->Vars;
+for my $k ( keys %vars ) {
+  $k =~ /^${pre}magic(\d+)$/ or next;
+  my $rownum = $1;
+  # find all submitted names ending in this rownum
+  my %thisrow = 
+    map { $_ => $vars{$_} } 
+    grep /^(.*[\d])$rownum$/, keys %vars;
+  $thisrow->{num} = delete $thisrow{"${pre}magic$rownum"};
+  push @rows, $thisrow;
+}
 </%doc>
-
-<TABLE ID="<% $prefix %>AutoTable" BGCOLOR="#cccccc" BORDER=0 CELLSPACING=0>
-  <TR>
-% foreach (@header) {
-    <TH><% $_ %></TH>
-% }
-  </TR>
-% my $row = 0;
-% for ( $row = 0; $row < scalar @data; $row++ ) {
-  <TR>
-%   my $col = 0;
-%   for ( $col = 0; $col < scalar @fields; $col++ ) {
-%     my $id = $prefix . $fields[$col];
-%     # don't suffix rownum in the final, blank row
-%     $id .= $row if $row < (scalar @data) - 1; 
-    <TD>
-%     my @o = @{ $select[$col] };
-%     if( @o ) {
-      <SELECT NAME="<% $id %>" ID="<% $id %>">
-%       while(@o) {
-%         my $val = shift @o;
-        <OPTION VALUE=<% $val %><% 
-$val eq $data[$row][$col] ? ' SELECTED' : ''%>><% shift @o %></OPTION>
-%       }
-      </SELECT>
-%     }
-%     else {
-      <INPUT TYPE      = "text"
-             NAME      = "<% $id %>"
-             ID        = "<% $id %>"
-             SIZE      = <% $size[$col] %>
-             MAXLENGTH = <% $maxl[$col] %>
-             STYLE     = "text-align:<% $align[$col] %>"
-             VALUE     = "<% $data[$row][$col] %>"
-%       if( $opt{'autoadd'} ) {
-             onchange  = "possiblyAddRow(this);"
-%       }
-      >
-    </TD>
-%     }
-%   }
-    <TD>
-      <IMG SRC     = "<% "${p}images/cross.png" %>" 
-           ALT     = "X" 
-           onclick = "deleteRow(this);"
-           >
-    </TD>
-  </TR>
-% }
-</TABLE>
-% if( !$opt{'autoadd'} ) {
-<INPUT TYPE="button" VALUE="Add" onclick="<% $prefix %>addRow();"><BR>
-% }
-
-<SCRIPT TYPE="text/javascript">
-  var <% $prefix %>rownum = <% $row %>;
-  var <% $prefix %>table = document.getElementById('<% $prefix %>AutoTable');
-  // last row is initially blank, clone it and remove it
-  var <% $prefix %>_blank = 
-    <% $prefix %>table.rows[<% $prefix %>table.rows.length-1].cloneNode(true);
-% if( !$opt{'autoadd'} ) {
-  <% $prefix %>table.deleteRow(<% $prefix %>table.rows.length-1);
-% }
-  
-    
-
-  function rownum_of(obj) {
-    return (obj.parentNode.parentNode.sectionRowIndex);
+<tbody id="<%$pre%>autotable"></tbody>
+<script type="text/javascript">
+var <%$pre%>template;
+var <%$pre%>tbody;
+var <%$pre%>next_rownum;
+var <%$pre%>set_rownum;
+var <%$pre%>addRow;
+var <%$pre%>deleteRow;
+var <%$pre%>fieldorder = <% to_json($fieldorder) %>;
+
+function <%$pre%>possiblyAddRow_factory(obj) {
+  var callback = obj.onchange;
+  return function() {
+    if ( obj.rownum == <%$pre%>tbody.lastChild.rownum ) {
+      // then this is the last row, and it's being changed, so spawn a new row
+      <%$pre%>addRow();
+    }
+    if ( callback ) {
+      callback.apply(obj);
+    }
   }
+}
 
-  function <% $prefix %>possiblyAddRow(obj) {
-    if ( <% $prefix %>rownum == rownum_of(obj) ) {
-      <% $prefix %>addRow();
+function <%$pre%>set_rownum(obj, rownum) {
+  obj.rownum = rownum;
+  if ( obj.id ) {
+    obj.id = obj.id + rownum;
+  }
+  if ( obj.name ) {
+    obj.name = obj.name + rownum;
+    // also, in this case it's a form field that will be part of the record
+    // so set up an onchange handler
+    obj.onchange = <%$pre%>possiblyAddRow_factory(obj);
+  }
+  for (var i = 0; i < obj.children.length; i++) {
+    if ( obj.children[i] instanceof Node ) {
+      <%$pre%>set_rownum(obj.children[i], rownum);
     }
   }
+}
 
-  function <% $prefix %>addRow() {
-    var row = <% $prefix %>table.insertRow(-1);
-    var cells = <% $prefix %>_blank.cells;
-    for (i=0; i<cells.length; i++) {
-      var node = row.appendChild(cells[i].cloneNode(true));
-      var input = node.children[0];
-      input.id = input.id + row.sectionRowIndex;
-      input.name = input.name + row.sectionRowIndex;
+function <%$pre%>addRow(data) {
+  // duplicate the node
+  // warning: cloneNode doesn't clone event handlers that were set through 
+  // the DOM
+  // if 'data' is an object, prepopulate the row's fields with the object's
+  // elements
+  // returns the rownum of the new row
+  var row = <%$pre%>template.cloneNode(true);
+  <%$pre%>tbody.appendChild(row);
+  var this_rownum = <%$pre%>next_rownum;
+  <%$pre%>set_rownum(row, this_rownum);
+  if(data instanceof Array) {
+    for (i = 0; i < data.length && i < <%$pre%>fieldorder.length; i++) {
+      var el = document.getElementsByName(<%$pre%>fieldorder[i] + this_rownum)[0];
+      if (el) {
+        el.value = data[i];
+      }
+    }
+  } else if (data instanceof Object) {
+    for (var field in data) {
+      var el = document.getElementsByName(field + this_rownum)[0];
+      if (el) {
+        el.value = data[field];
+%       # doesn't work for checkbox
+      }
     }
-    <% $prefix %>rownum++;
+  } // else nothing
+  <%$pre%>next_rownum++;
+  return this_rownum;
+}
+
+function <%$pre%>deleteRow(rownum) {
+  if ( rownum == <%$pre%>tbody.lastChild.rownum ) {
+    // if this is the last row, spawn another one after it
+    <%$pre%>addRow();
   }
+  var r = document.getElementById('<%$pre%>row' + rownum);
+  <%$pre%>tbody.removeChild(r);
+}
 
-  function deleteRow(obj) {
-    if(<% $prefix %>rownum == rownum_of(obj))  {
-      <% $prefix %>addRow();
-    }
-    <% $prefix %>table.deleteRow(rownum_of(obj));
-    <% $prefix %>rownum--;
-    return(false);
+function <%$pre%>init() {
+  <%$pre%>template = document.getElementById(<% $template_row |js_string%>);
+  <%$pre%>tbody = document.getElementById('<%$pre%>autotable');
+  <%$pre%>next_rownum = <%$pre%>template.sectionRowIndex;
+  // detach the template row
+  var table = <%$pre%>template.parentNode;
+  table.removeChild(<%$pre%>template);
+  // give it an id
+  <%$pre%>template.id = <%$pre |js_string%> + 'row';
+  // and a magic identifier so we know it's been submitted
+  var magic = document.createElement('INPUT');
+  magic.setAttribute('type', 'hidden');
+  magic.setAttribute('name', '<%$pre%>magic');
+  magic.value = '1';
+  // and a delete button
+%# should this be enclosed in an actual <button> for aesthetics?
+  var delete_button = document.createElement('IMG');
+  delete_button.id = 'delete_button';
+  delete_button.src = '<%$fsurl%>images/cross.png';
+  delete_button.alt = 'X';
+  // use an inline string for this so that it will be cloned properly
+  delete_button.setAttribute('onclick', "<%$pre%>deleteRow(this.rownum);");
+  var delete_cell = document.createElement('TD');
+  delete_cell.appendChild(delete_button);
+  delete_cell.appendChild(magic); // it has to go somewhere
+  <%$pre%>template.appendChild(delete_cell);
+
+  // preload rows
+  var rows = <% to_json(\@rows) %>;
+  for (var i = 0; i < rows.length; i++) {
+    <%$pre%>addRow(rows[i]);
   }
 
-</SCRIPT>
+  <%$pre%>addRow();
+}
 
+<%$pre%>init();
+</script>
 <%init>
 my %opt = @_;
-
-my @header = @{ $opt{'header'} };
-my @fields = @{ $opt{'fields'} };
-my @data = ();
-if($opt{'data'}) {
-  @data = @{ $opt{'data'} };
-}
-elsif($opt{'records'}) {
-  foreach my $rec (@{ $opt{'records'} }) {
-    push @data, [ map { $rec->getfield($_) } @fields ];
+my $pre = '';
+$pre = $opt{'table'} . '_' if $opt{'table'};
+my $template_row = $opt{'template_row'}
+  or die "auto-table requires template_row\n"; # a DOM id
+
+# rows that we will preload, as hashrefs of name => value
+my @rows = @{ $opt{'data'} || [] };
+foreach (@rows) {
+  # allow an array of FS::Record objects to be passed
+  if ( blessed($_) and $_->isa('FS::Record') ) {
+    $_ = $_->hashref;
   }
 }
-# else @data = ();
-push @data, [ map {''} @fields ]; # make a blank row
-
-my $prefix = $opt{'prefix'};
-my @size = $opt{'size'} ? @{ $opt{'size'} } : (map {16} @fields);
-my @maxl = $opt{'maxl'} ? @{ $opt{'maxl'} } : @size;
-my @align = $opt{'align'} ? @{ $opt{'align'} } : (map {'right'} @fields);
-my @select = @{ $opt{'select'} || [] };
-foreach (0..scalar(@fields)-1) {
-  $select[$_] ||= [];
-}
+my $fieldorder = $opt{'fieldorder'} || [];
 </%init>
diff --git a/httemplate/elements/select-mib-popup.html b/httemplate/elements/select-mib-popup.html
new file mode 100644
index 0000000..bd485ef
--- /dev/null
+++ b/httemplate/elements/select-mib-popup.html
@@ -0,0 +1,186 @@
+<& /elements/header-popup.html &>
+<DIV STYLE="visibility: hidden; position: absolute" ID="measurebox"></DIV>
+<TABLE WIDTH="100%">
+<TR>
+  <TD WIDTH="30%" ALIGN="right">Module:</TD>
+  <TD><SELECT ID="select_module"></SELECT></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Object:</TD>
+  <TD><INPUT TYPE="text" NAME="path" ID="input_path" WIDTH="100%"></TD>
+</TR>
+<TR>
+  <TD COLSPAN=2>
+    <SELECT STYLE="width:100%" SIZE=12 ID="select_path"></SELECT>
+  </TD>
+</TR>
+<TR>
+  <TH ALIGN="center" COLSPAN=2 ID="mib_objectID"></TH>
+</TR>
+<TR>
+  <TD ALIGN="right">Module: </TD><TD ID="mib_moduleID"></TD>
+</TR>
+<TR>
+  <TD ALIGN="right">Data type: </TD><TD ID="mib_type"></TD>
+</TR>
+<TR>
+  <TH COLSPAN=2>
+    <BUTTON ID="submit_button" onclick="submit()" DISABLED=1>Continue</BUTTON>
+  </TH>
+</TR>
+</TABLE>
+<& /elements/xmlhttp.html,
+  url   => $p.'misc/xmlhttp-mib-browse.html',
+  subs  => [qw( search get_module_list )],
+&>
+<SCRIPT TYPE="text/javascript">
+
+var selected_mib;
+
+function show_info(state) {
+  document.getElementById('mib_objectID').style.display = 
+    document.getElementById('mib_moduleID').style.display = 
+    document.getElementById('mib_type').style.display = 
+    state ? '' : 'none';
+}
+
+function clear_list() {
+  var select_path = document.getElementById('select_path');
+  select_path.options.length = 0;
+}
+
+var measurebox = document.getElementById('measurebox');
+function add_item(value) {
+  var select_path = document.getElementById('select_path');
+  var input_path = document.getElementById('input_path');
+  var opt = document.createElement('option');
+  var v = value;
+  if ( v.match(/-$/) ) {
+    opt.className = 'leaf';
+    v = v.substring(0, v.length - 1);
+  }
+  var optvalue = v; // may not be the name we display
+  // shorten these if they don't fit in the box
+  if ( v.length > 30 ) { // unless they're already really short
+    measurebox.innerHTML = v;
+    while ( measurebox.clientWidth > select_path.clientWidth - 10
+            && v.match(/^\..*\./) ) {
+      v = v.replace(/^\.[^\.]+/, '');
+      measurebox.innerHTML = v;
+    }
+    if ( optvalue != v ) {
+      v = '...' + v;
+    }
+  }
+  opt.value = optvalue;
+  opt.text = v;
+  opt.selected = (input_path.value == v);
+  select_path.add(opt, null);
+}
+
+var timerID = 0;
+
+function populate(json_result) {
+  var result = JSON.parse(json_result);
+  clear_list();
+  for (var x in result['choices']) {
+    opt = document.createElement('option');
+    add_item(result['choices'][x]);
+  }
+  if ( result['objectID'] ) {
+    selected_mib = result;
+    show_info(true);
+    // show details on the selected node
+    document.getElementById('mib_objectID').innerHTML = result.objectID;
+    document.getElementById('mib_moduleID').innerHTML = result.moduleID;
+    document.getElementById('mib_type').innerHTML = result.type;
+    document.getElementById('submit_button').disabled = !result.type;
+  } else {
+    selected_mib = undefined;
+    show_info(false);
+  }
+}
+
+function populate_modules(json_result) {
+  var result = JSON.parse(json_result);
+  var select_module = document.getElementById('select_module');
+  var opt = document.createElement('option');
+  opt.value = 'ANY';
+  opt.text  = '(any)';
+  select_module.add(opt, null);
+  for (var x in result['modules']) {
+    opt = document.createElement('option');
+    opt.value = opt.text = result['modules'][x];
+    select_module.add(opt, null);
+  }
+}
+
+function dispatch_search() {
+  // called from the interval timer
+  var search_string = document.getElementById('select_module').value + ':' +
+                      document.getElementById('input_path').value;
+
+  search(search_string, populate);
+}
+
+function delayed_search() {
+  // onkeyup handler for the text input
+  // 500ms after the user stops typing, send the search request
+  if (timerID != 0) {
+    clearTimeout(timerID);
+  }
+  timerID = setTimeout(dispatch_search, 500);
+}
+
+function handle_choose_object() {
+  // onchange handler for the selector
+  // when the user picks an option, set the text input to that, and then
+  // search for it as though it was entered
+  var input_path = document.getElementById('input_path');
+  input_path.value = this.value;
+  dispatch_search();
+}
+
+function handle_choose_module() {
+  input_path.value = ''; // just to avoid confusion
+  delayed_search();
+}
+
+function submit() {
+% if ( $callback ) {
+  <% $callback %>;
+  parent.nd(1); // close popup
+% } else {
+  alert(document.getElementById('input_path').value);
+% }
+}
+
+var input_path = document.getElementById('input_path');
+input_path.onkeyup = delayed_search;
+var select_path = document.getElementById('select_path');
+select_path.onchange = handle_choose_object;
+var select_module = document.getElementById('select_module');
+select_module.onchange = handle_choose_module;
+% if ( $cgi->param('curr_value') ) {
+input_path.value = <% $cgi->param('curr_value') |js_string %>;
+% }
+dispatch_search();
+get_module_list('', populate_modules);
+
+</SCRIPT>
+<& /elements/footer.html &>
+<%init>
+my $callback = 'alert("(no callback defined)" + selected_mib.stringify)';
+$cgi->param('callback') =~ /^(\w+)$/;
+if ( $1 ) {
+  # construct the JS function call expresssion
+  $callback = 'window.parent.' . $1 . '(selected_mib';
+  foreach ($cgi->param('arg')) {
+    # pass-through arguments
+    /^(\w+)$/ or next;
+    $callback .= ",'$1'";
+  }
+  $callback .= ')';
+}
+
+</%init>
diff --git a/httemplate/elements/xmlhttp.html b/httemplate/elements/xmlhttp.html
index ac6f991..a9e65c7 100644
--- a/httemplate/elements/xmlhttp.html
+++ b/httemplate/elements/xmlhttp.html
@@ -14,14 +14,15 @@ Example:
   );
 
 </%doc>
-<% include( '/elements/rs_init_object.html' ) %>
+<& /elements/rs_init_object.html &>
+<& /elements/init_overlib.html &>
 <SCRIPT TYPE="text/javascript">
 
 % foreach my $func ( @{$opt{'subs'}} ) { 
 %
 %       my $furl = $url;
 %       $furl =~ s/\"/\\\\\"/; #javascript escape
-%
+%#"
 %  
 
 
@@ -66,15 +67,26 @@ Example:
             } else {
               var data = xmlhttp.responseText;
               //alert('received response: ' + data);
-              a[a.length-1](data);
               if ( data.indexOf("<b>System error</b>") > -1 ) {
-                var w;
-                if ( w = window.open("about:blank") ) {
-                  w.document.write(data);
-                } else {
-                  // popup blocking?  should use an overlib popup instead 
-                  alert("Error popup disabled; try disabling popup blocking to see");
-                }
+                // trim this a little
+                var end = data.indexOf('<a href="#raw">') - 1;
+                data = data.substring(0, end);
+
+                overlib(data,
+                  WIDTH, 480, MIDX, 0, MIDY, 0,
+                  CAPTION, 'Error', STICKY, AUTOSTATUSCAP, DRAGGABLE,
+                  CLOSECLICK, BGCOLOR, '#f00', CGCOLOR, '#f00'
+                );
+                //var w;
+                //if ( w = window.open("about:blank") ) {
+                //  w.document.write(data);
+                //} else {
+                //  // popup blocking?  should use an overlib popup instead 
+                //  alert("Error popup disabled; try disabling popup blocking to see");
+                //}
+              } else {
+                // invoke the callback
+                a[a.length-1](data);
               }
             }
         }
diff --git a/httemplate/misc/xmlhttp-mib-browse.html b/httemplate/misc/xmlhttp-mib-browse.html
new file mode 100644
index 0000000..f3084ff
--- /dev/null
+++ b/httemplate/misc/xmlhttp-mib-browse.html
@@ -0,0 +1,161 @@
+%#<% Data::Format::HTML->new->format($index{by_path}) %>
+% my $json = "JSON"->new->canonical;
+<% $json->encode($result) %>
+<%init>
+#<%once>  #enable me in production
+use SNMP;
+SNMP::initMib();
+my $mib = \%SNMP::MIB;
+
+# make an index of the leaf nodes
+my %index = (
+  by_objectID => {}, # {.1.3.6.1.2.1.1.1}
+  by_fullname => {}, # {iso.org.dod.internet.mgmt.mib-2.system.sysDescr}
+  by_path     => {}, # {iso}{org}{dod}{internet}{mgmt}{mib-2}{system}{sysDescr}
+  module  => {}, #{SNMPv2-MIB}{by_path}{iso}{org}...
+                 #{SNMPv2-MIB}{by_fullname}{iso.org...}
+);
+
+my %name_of_oid = (); # '.1.3.6.1' => 'iso.org.dod.internet'
+
+# build up path names
+my $fullname;
+$fullname = sub {
+  my $oid = shift;
+  return $name_of_oid{$oid} if exists $name_of_oid{$oid};
+
+  my $object = $mib->{$oid};
+  my $myname = '.' . $object->{label};
+  # cut off the last element and recurse
+  $oid =~ /^(\.[\d\.]+)?(\.\d+)$/;
+  if ( length($1) ) {
+    $myname = $fullname->($1) . $myname;
+  }
+  return $name_of_oid{$oid} = $myname
+};
+
+my @oids = keys(%$mib); # dotted numeric OIDs
+foreach my $oid (@oids) {
+  my $object = {};
+  %$object = %{ $mib->{$oid} }; # untie it
+  # and remove references
+  delete $object->{parent};
+  delete $object->{children};
+  delete $object->{nextNode};
+  $index{by_objectID}{$oid} = $object;
+  my $myname = $fullname->($oid);
+  $object->{fullname} = $myname;
+  $index{by_fullname}{$myname} = $object;
+  my $moduleID = $object->{moduleID};
+  $index{module}{$moduleID} ||= { by_fullname => {}, by_path => {} };
+  $index{module}{$moduleID}{by_fullname}{$myname} = $object;
+}
+my @names = sort {$a cmp $b} keys %{ $index{by_fullname} };
+foreach my $myname (@names) {
+  my $obj = $index{by_fullname}{$myname};
+  my $moduleID = $obj->{moduleID};
+  my @parts = split('\.', $myname);
+  shift @parts; # always starts with an empty string
+  for ($index{by_path}, $index{module}{$moduleID}{by_path}) {
+    my $subindex = $_;
+    for my $this_part (@parts) {
+      $subindex = $subindex->{$this_part} ||= {};
+    }
+    # $subindex now = $index{by_path}{foo}{bar}{baz}.
+    # set {''} = the object with that name.
+    # and set object $index{by_path}{foo}{bar}{baz}{''} = 
+    # the object named .foo.bar.baz
+    $subindex->{''} = $obj;
+  }
+}
+
+#</%once>
+#<%init>
+# no ACL for this
+my $sub = $cgi->param('sub');
+my $result = {};
+if ( $sub eq 'search' ) {
+  warn "search: ".$cgi->param('arg')."\n";
+  my ($module, $string) = split(':', $cgi->param('arg'), 2);
+  my $idx; # the branch of the index to use for this search
+  if ( $module eq 'ANY' ) {
+    $idx = \%index;
+  } elsif (exists($index{module}{$module}) ) {
+    $idx = $index{module}{$module};
+  } else {
+    warn "unknown MIB moduleID: $module\n";
+    $idx = {}; # will return nothing, because you've somehow sent a bad moduleID
+  }
+  if ( exists($index{by_fullname}{$string}) ) {
+    warn "exact match\n";
+    # don't make this module-selective--if the path matches an existing 
+    # object, return that object
+    %$result = %{ $index{by_fullname}{$string} }; # put the object info in $result
+    #warn Dumper $result;
+  }
+  my @choices; # menu options to return
+  if ( $string =~ /^[\.\d]+$/ ) {
+    # then this is a numeric path
+    # ignore the module filter, and return everything starting with $string
+    if ( $string =~ /^\./ ) {
+      @choices = grep /^\Q$string\E/, keys %{$index{by_objectID}};
+    } else {
+      # or everything containing it
+      @choices = grep /\Q$string\E/, keys %{$index{by_objectID}};
+    }
+    @choices = map { $index{by_objectID}{$_}->{fullname} } @choices;
+  } elsif ( $string eq '' or $string =~ /^\./ ) {
+    # then this is an absolute path
+    my @parts = split('\.', $string);
+    shift @parts;
+    my $subindex = $idx->{by_path};
+    my $path = '';
+    @choices = keys %$subindex;
+    # walk all the specified path parts
+    foreach my $this_part (@parts) {
+      # stop before walking off the map
+      last if !exists($subindex->{$this_part});
+      $subindex = $subindex->{$this_part};
+      $path .= '.' . $this_part;
+      @choices = grep {$_} keys %$subindex;
+    }
+    # skip uninteresting nodes: those that aren't accessible nodes (have no
+    # data type), and have only one path forward
+    while ( scalar(@choices) == 1
+            and (!exists $subindex->{''} or $subindex->{''}->{type} eq '') ) {
+
+      $subindex = $subindex->{ $choices[0] };
+      $path .= '.' . $choices[0];
+      @choices = grep {$_} keys %$subindex;
+
+    }
+
+    # if we are on an existing node, and the entered path didn't exactly
+    # match another node, return the current node as the result
+    if (!keys %$result and exists($subindex->{''})) {
+      %$result = %{ $subindex->{''} };
+    }
+    # prepend the path up to this point
+    foreach (@choices) {
+      $_ = $path.'.'.$_;
+      # also label accessible nodes for the UI
+      if ( exists($subindex->{$_}{''}) and $subindex->{$_}{''}{'type'} ) {
+        $_ .= '-';
+      }
+    }
+    # also include one level above the originally requested path, 
+    # for tree-like navigation
+    if ( $string =~ /^(.+)\.[^\.]+/ ) {
+      unshift @choices, $1;
+    }
+  } else {
+    # then this is a full-text search
+    warn "/$string/\n";
+    @choices = grep /\Q$string\E/i, keys(%{ $idx->{by_fullname} });
+  }
+  @choices = sort @choices;
+  $result->{choices} = \@choices;
+} elsif ( $sub eq 'get_module_list' ) {
+  $result = { modules => [ sort keys(%{ $index{module} }) ] };
+}
+</%init>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Mason.pm                                     |    1 +
 FS/FS/part_export.pm                               |   13 +
 FS/FS/part_export/broadband_snmp.pm                |  150 ++++++----
 httemplate/browse/part_export.cgi                  |   48 +++-
 httemplate/edit/cdr_type.cgi                       |   22 +-
 .../edit/elements/part_export/broadband_snmp.html  |  101 +++++++
 httemplate/edit/elements/part_export/foot.html     |    6 +
 httemplate/edit/elements/part_export/head.html     |   13 +
 httemplate/edit/part_export.cgi                    |    9 +
 httemplate/edit/process/cdr_type.cgi               |    1 -
 httemplate/edit/process/part_export.cgi            |   29 ++-
 httemplate/edit/rate_time.cgi                      |   41 +++-
 httemplate/elements/auto-table.html                |  310 ++++++++++----------
 httemplate/elements/select-mib-popup.html          |  186 ++++++++++++
 httemplate/elements/xmlhttp.html                   |   32 ++-
 httemplate/misc/xmlhttp-mib-browse.html            |  161 ++++++++++
 16 files changed, 887 insertions(+), 236 deletions(-)
 create mode 100644 httemplate/edit/elements/part_export/broadband_snmp.html
 create mode 100644 httemplate/edit/elements/part_export/foot.html
 create mode 100644 httemplate/edit/elements/part_export/head.html
 create mode 100644 httemplate/elements/select-mib-popup.html
 create mode 100644 httemplate/misc/xmlhttp-mib-browse.html




More information about the freeside-commits mailing list