diff --git a/Netdisco/Changes b/Netdisco/Changes index 35c42d87..191a9079 100644 --- a/Netdisco/Changes +++ b/Netdisco/Changes @@ -1,3 +1,22 @@ +2.011000 - 2013-07-29 + + [NEW FEATURES] + + * Port Utilization report + * User Management (for admins only) + + [ENHANCEMENTS] + + * Add docs note about SSL support + * Button to empty the job queue, and improve display when the queue is empty + * Table headers float on the page when scrolling + + [BUG FIXES] + + * REMOTE_USER is an env var, not an HTTP Header + * Swap play/pause icons in jobqueue + * Find the RW snmp community string correctly now + 2.010004 - 2013-07-24 [BUG FIXES] diff --git a/Netdisco/MANIFEST b/Netdisco/MANIFEST index fd20ace8..c43326a2 100644 --- a/Netdisco/MANIFEST +++ b/Netdisco/MANIFEST @@ -72,6 +72,7 @@ lib/App/Netdisco/DB/Result/Virtual/DevicePortVlanNative.pm lib/App/Netdisco/DB/Result/Virtual/DevicePortVlanTagged.pm lib/App/Netdisco/DB/Result/Virtual/DuplexMismatch.pm lib/App/Netdisco/DB/Result/Virtual/NodeWithAge.pm +lib/App/Netdisco/DB/Result/Virtual/PortUtilization.pm lib/App/Netdisco/DB/ResultSet/Admin.pm lib/App/Netdisco/DB/ResultSet/Device.pm lib/App/Netdisco/DB/ResultSet/DevicePort.pm @@ -98,6 +99,7 @@ lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-2-PostgreSQL.sql lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-20-21-PostgreSQL.sql lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-21-22-PostgreSQL.sql lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-22-23-PostgreSQL.sql +lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-23-24-PostgreSQL.sql lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-3-4-PostgreSQL.sql lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-4-5-PostgreSQL.sql lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-5-6-PostgreSQL.sql @@ -127,6 +129,7 @@ lib/App/Netdisco/Web/Plugin.pm lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm lib/App/Netdisco/Web/Plugin/AdminTask/PseudoDevice.pm lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm +lib/App/Netdisco/Web/Plugin/AdminTask/Users.pm lib/App/Netdisco/Web/Plugin/Device/Addresses.pm lib/App/Netdisco/Web/Plugin/Device/Details.pm lib/App/Netdisco/Web/Plugin/Device/Modules.pm @@ -134,6 +137,7 @@ lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm lib/App/Netdisco/Web/Plugin/Device/Ports.pm lib/App/Netdisco/Web/Plugin/Inventory.pm lib/App/Netdisco/Web/Plugin/Report/DuplexMismatch.pm +lib/App/Netdisco/Web/Plugin/Report/PortUtilization.pm lib/App/Netdisco/Web/Plugin/Search/Device.pm lib/App/Netdisco/Web/Plugin/Search/Node.pm lib/App/Netdisco/Web/Plugin/Search/Port.pm @@ -194,19 +198,23 @@ share/public/javascripts/jquery-deserialize.js share/public/javascripts/jquery-history.js share/public/javascripts/jquery-latest.min.js share/public/javascripts/jquery-ui.custom.min.js +share/public/javascripts/jquery.floatThead.js share/public/javascripts/jquery.qtip.min.js share/public/javascripts/netdisco.js share/public/javascripts/netdisco_portcontrol.js share/public/javascripts/toastr.js +share/public/javascripts/underscore.min.js share/views/admintask.tt share/views/ajax/admintask/jobqueue.tt share/views/ajax/admintask/pseudodevice.tt share/views/ajax/admintask/topology.tt +share/views/ajax/admintask/users.tt share/views/ajax/device/addresses.tt share/views/ajax/device/details.tt share/views/ajax/device/netmap.tt share/views/ajax/device/ports.tt share/views/ajax/report/duplexmismatch.tt +share/views/ajax/report/portutilization.tt share/views/ajax/search/device.tt share/views/ajax/search/node_by_ip.tt share/views/ajax/search/node_by_mac.tt diff --git a/Netdisco/META.yml b/Netdisco/META.yml index 01deb05b..b0a38186 100644 --- a/Netdisco/META.yml +++ b/Netdisco/META.yml @@ -60,4 +60,4 @@ resources: homepage: http://netdisco.org/ license: http://opensource.org/licenses/bsd-license.php repository: git://git.code.sf.net/p/netdisco/netdisco-ng -version: 2.010004 +version: 2.011000 diff --git a/Netdisco/lib/App/Netdisco.pm b/Netdisco/lib/App/Netdisco.pm index 8eeb82bc..44ed3d75 100644 --- a/Netdisco/lib/App/Netdisco.pm +++ b/Netdisco/lib/App/Netdisco.pm @@ -7,7 +7,7 @@ use 5.010_000; use File::ShareDir 'dist_dir'; use Path::Class; -our $VERSION = '2.010004'; +our $VERSION = '2.011000'; BEGIN { if (not ($ENV{DANCER_APPDIR} || '') diff --git a/Netdisco/lib/App/Netdisco/DB.pm b/Netdisco/lib/App/Netdisco/DB.pm index b40ebbab..7663003e 100644 --- a/Netdisco/lib/App/Netdisco/DB.pm +++ b/Netdisco/lib/App/Netdisco/DB.pm @@ -8,7 +8,7 @@ use base 'DBIx::Class::Schema'; __PACKAGE__->load_namespaces; -our $VERSION = 23; # schema version used for upgrades, keep as integer +our $VERSION = 24; # schema version used for upgrades, keep as integer use Path::Class; use File::Basename; diff --git a/Netdisco/lib/App/Netdisco/DB/Result/Device.pm b/Netdisco/lib/App/Netdisco/DB/Result/Device.pm index d0b81e24..dc9b4cd1 100644 --- a/Netdisco/lib/App/Netdisco/DB/Result/Device.pm +++ b/Netdisco/lib/App/Netdisco/DB/Result/Device.pm @@ -65,6 +65,8 @@ __PACKAGE__->add_columns( { data_type => "integer", is_nullable => 1 }, "snmp_comm", { data_type => "text", is_nullable => 1 }, + "snmp_comm_rw", + { data_type => "text", is_nullable => 1 }, "snmp_class", { data_type => "text", is_nullable => 1 }, "vtp_domain", diff --git a/Netdisco/lib/App/Netdisco/DB/Result/Virtual/PortUtilization.pm b/Netdisco/lib/App/Netdisco/DB/Result/Virtual/PortUtilization.pm new file mode 100644 index 00000000..a37c383a --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/Result/Virtual/PortUtilization.pm @@ -0,0 +1,46 @@ +package App::Netdisco::DB::Result::Virtual::PortUtilization; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +__PACKAGE__->table_class('DBIx::Class::ResultSource::View'); + +__PACKAGE__->table('port_utilization'); +__PACKAGE__->result_source_instance->is_virtual(1); +__PACKAGE__->result_source_instance->view_definition(<add_columns( + 'dns' => { + data_type => 'text', + }, + 'ip' => { + data_type => 'inet', + }, + 'port_count' => { + data_type => 'integer', + }, + 'ports_in_use' => { + data_type => 'integer', + }, + 'ports_shutdown' => { + data_type => 'integer', + }, + 'ports_free' => { + data_type => 'integer', + }, +); + +1; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm index 5c7bb7c4..2f8066e3 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm @@ -511,12 +511,16 @@ sub with_port_count { ->search_rs($cond, $attrs) ->search({}, { - '+columns' => { port_count => - $rs->result_source->schema->resultset('DevicePort') - ->search( - { 'dp.ip' => { -ident => 'me.ip' } }, - { alias => 'dp' } - )->count_rs->as_query + '+columns' => { + port_count => + $rs->result_source->schema->resultset('DevicePort') + ->search( + { + 'dp.ip' => { -ident => 'me.ip' }, + 'dp.type' => { '!=' => 'propVirtual' }, + }, + { alias => 'dp' } + )->count_rs->as_query, }, }); } diff --git a/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-23-24-PostgreSQL.sql b/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-23-24-PostgreSQL.sql new file mode 100644 index 00000000..341c15f8 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-23-24-PostgreSQL.sql @@ -0,0 +1,5 @@ +BEGIN; + +ALTER TABLE device ADD COLUMN snmp_comm_rw text; + +COMMIT; diff --git a/Netdisco/lib/App/Netdisco/Manual/Configuration.pod b/Netdisco/lib/App/Netdisco/Manual/Configuration.pod index ccff5731..b01ca311 100644 --- a/Netdisco/lib/App/Netdisco/Manual/Configuration.pod +++ b/Netdisco/lib/App/Netdisco/Manual/Configuration.pod @@ -101,8 +101,9 @@ Value: Boolean. Default: C. Enable this if Netdisco is running within another web server such as Apache, and you want that server to handle user authentication. Normally the -authenticated username will automatically be set in the C HTTP -Header. See L for further details. +authenticated username will automatically be set in the C +environment variable. See L for +further details. =head3 C @@ -110,8 +111,8 @@ Value: Boolean. Default: C. Enable this if you proxy requests to Netdisco via another web server such as Apache, and you want that server to handle user authentication. You need to -configure the authorized username to be passed in the C HTTP -Header. For example with Apache: +configure the authorized username to be passed from the frontend environment +to Netdisco in the C HTTP Header. For example with Apache: RequestHeader unset X-REMOTE_USER RequestHeader set X-REMOTE_USER "%{REMOTE_USER}e" env=REMOTE_USER diff --git a/Netdisco/lib/App/Netdisco/Manual/Deployment.pod b/Netdisco/lib/App/Netdisco/Manual/Deployment.pod index 8cb87cc4..0b14c7cc 100644 --- a/Netdisco/lib/App/Netdisco/Manual/Deployment.pod +++ b/Netdisco/lib/App/Netdisco/Manual/Deployment.pod @@ -75,6 +75,16 @@ To delegate user authentication to Apache, use the C or C settings. See L for more details. +=head1 SSL Support + +There is no SSL support in the built-in web server. This is because it's not +straightforward to support all the SSL options, and using port 443 requires +root privilege, which the Netdisco application should not have. + +You are instead recommended to run C behind a reverse proxy as +described elsewhere in this document. Apache can easily act as an SSL reverse +proxy. + =head1 SQL and HTTP Trace For SQL debugging try the following commands: diff --git a/Netdisco/lib/App/Netdisco/Manual/ReleaseNotes.pod b/Netdisco/lib/App/Netdisco/Manual/ReleaseNotes.pod index 642ecd19..c2932f1d 100644 --- a/Netdisco/lib/App/Netdisco/Manual/ReleaseNotes.pod +++ b/Netdisco/lib/App/Netdisco/Manual/ReleaseNotes.pod @@ -21,8 +21,8 @@ the more verbose C setting which was there before: user: 'someuser' pass: 'somepass' -Also, the C and C environment variables are now -supported for delegating authentication to another web server. See the +Also, the C environment variable and C HTTP Header +are now supported for delegating authentication to another web server. See the Deployment and Configuration documentation for further details. =head1 2.008000 diff --git a/Netdisco/lib/App/Netdisco/Util/SNMP.pm b/Netdisco/lib/App/Netdisco/Util/SNMP.pm index 90d3fcf1..3c7ddaf4 100644 --- a/Netdisco/lib/App/Netdisco/Util/SNMP.pm +++ b/Netdisco/lib/App/Netdisco/Util/SNMP.pm @@ -90,6 +90,9 @@ sub _snmp_connect_generic { unshift @communities, $device->snmp_comm if defined $device->snmp_comm and defined $comm_type and $comm_type eq 'community'; + unshift @communities, $device->snmp_comm_rw + if defined $device->snmp_comm_rw + and defined $comm_type and $comm_type eq 'community_rw'; my $info = undef; VERSION: foreach my $ver (@versions) { @@ -101,8 +104,13 @@ sub _snmp_connect_generic { COMMUNITY: foreach my $comm (@communities) { next unless $comm; - $info = _try_connect($ver, $class, $comm, \%snmp_args) - and last VERSION; + $info = _try_connect($ver, $class, $comm, \%snmp_args); + + if ($comm_type eq 'community_rw') { + _try_write($info, $comm, $device) or next COMMUNITY; + } + + last VERSION if $info; } } } @@ -110,6 +118,25 @@ sub _snmp_connect_generic { return $info; } +sub _try_write { + my ($info, $comm, $device) = @_; + my $happy = 0; + + try { + debug sprintf '[%s] try_write with comm: %s', $device->ip, $comm; + $info->clear_cache; + my $rv = $info->set_location( $info->location ); + $device->update({snmp_comm_rw => $comm}) + if $device->in_storage; + $happy = 1 if $rv; + } + catch { + debug $_; + }; + + return $happy; +} + sub _try_connect { my ($ver, $class, $comm, $snmp_args) = @_; my $info = undef; @@ -154,7 +181,7 @@ sub _build_mibdirs { sub _get_mibdirs_content { my $home = shift; - warning 'Netdisco SNMP work will be slow - loading ALL MIBs. Consider setting mibdirs.'; + # warning 'Netdisco SNMP work will be slow - loading ALL MIBs. Consider setting mibdirs.'; my @list = map {s|$home/||; $_} grep {-d} glob("$home/*"); return \@list; } diff --git a/Netdisco/lib/App/Netdisco/Web/AuthN.pm b/Netdisco/lib/App/Netdisco/Web/AuthN.pm index b113d4f2..bad88259 100644 --- a/Netdisco/lib/App/Netdisco/Web/AuthN.pm +++ b/Netdisco/lib/App/Netdisco/Web/AuthN.pm @@ -10,8 +10,8 @@ hook 'before' => sub { if (setting('trust_x_remote_user') and scalar request->header('X-REMOTE_USER')) { session(user => scalar request->header('X-REMOTE_USER')); } - elsif (setting('trust_remote_user') and scalar request->header('REMOTE_USER')) { - session(user => scalar request->header('REMOTE_USER')); + elsif (setting('trust_remote_user') and $ENV{REMOTE_USER}) { + session(user => $ENV{REMOTE_USER}); } elsif (setting('no_auth')) { session(user => 'guest'); diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm index ed111798..b539a18e 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm @@ -21,6 +21,14 @@ ajax '/ajax/control/admin/jobqueue/del' => sub { }); }; +ajax '/ajax/control/admin/jobqueue/delall' => sub { + send_error('Forbidden', 403) unless var('user')->admin; + + schema('netdisco')->txn_do(sub { + my $device = schema('netdisco')->resultset('Admin')->delete; + }); +}; + ajax '/ajax/content/admin/jobqueue' => sub { send_error('Forbidden', 403) unless var('user')->admin; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Users.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Users.pm new file mode 100644 index 00000000..39746033 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Users.pm @@ -0,0 +1,80 @@ +package App::Netdisco::Web::Plugin::AdminTask::Users; + +use Dancer ':syntax'; +use Dancer::Plugin::Ajax; +use Dancer::Plugin::DBIC; + +use App::Netdisco::Web::Plugin; +use Digest::MD5 (); + +register_admin_task({ + tag => 'users', + label => 'User Management', +}); + +sub _sanity_ok { + return 0 unless var('user') and var('user')->admin; + + return 0 unless param('username') + and param('username') =~ m/^[[:print:]]+$/ + and param('username') !~ m/[[:space:]]/; + + return 1; +} + +ajax '/ajax/control/admin/users/add' => sub { + send_error('Bad Request', 400) unless _sanity_ok(); + + schema('netdisco')->txn_do(sub { + my $user = schema('netdisco')->resultset('User') + ->create({ + username => param('username'), + password => Digest::MD5::md5_hex(param('password')), + fullname => param('fullname'), + port_control => (param('port_control') ? \'true' : \'false'), + admin => (param('admin') ? \'true' : \'false'), + }); + }); +}; + +ajax '/ajax/control/admin/users/del' => sub { + send_error('Bad Request', 400) unless _sanity_ok(); + + schema('netdisco')->txn_do(sub { + schema('netdisco')->resultset('User') + ->find({username => param('username')})->delete; + }); +}; + +ajax '/ajax/control/admin/users/update' => sub { + send_error('Bad Request', 400) unless _sanity_ok(); + + schema('netdisco')->txn_do(sub { + my $user = schema('netdisco')->resultset('User') + ->find({username => param('username')}); + return unless $user; + + $user->update({ + ((param('password') ne '********') + ? (password => Digest::MD5::md5_hex(param('password'))) + : ()), + fullname => param('fullname'), + port_control => (param('port_control') ? \'true' : \'false'), + admin => (param('admin') ? \'true' : \'false'), + }); + }); +}; + +ajax '/ajax/content/admin/users' => sub { + send_error('Forbidden', 403) unless var('user')->admin; + + my $set = schema('netdisco')->resultset('User') + ->search(undef, { order_by => [qw/fullname username/]}); + + content_type('text/html'); + template 'ajax/admintask/users.tt', { + results => $set, + }, { layout => undef }; +}; + +true; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Report/PortUtilization.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Report/PortUtilization.pm new file mode 100644 index 00000000..b7a70431 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Report/PortUtilization.pm @@ -0,0 +1,25 @@ +package App::Netdisco::Web::Plugin::Report::PortUtilization; + +use Dancer ':syntax'; +use Dancer::Plugin::Ajax; +use Dancer::Plugin::DBIC; + +use App::Netdisco::Web::Plugin; + +register_report({ + category => 'Device', + tag => 'portutilization', + label => 'Port Utilization', +}); + +ajax '/ajax/content/report/portutilization' => sub { + return unless schema('netdisco')->resultset('Device')->count; + my $set = schema('netdisco')->resultset('Virtual::PortUtilization'); + + content_type('text/html'); + template 'ajax/report/portutilization.tt', { + results => $set, + }, { layout => undef }; +}; + +true; diff --git a/Netdisco/share/config.yml b/Netdisco/share/config.yml index 01dfb3c1..97770a9a 100644 --- a/Netdisco/share/config.yml +++ b/Netdisco/share/config.yml @@ -23,10 +23,12 @@ path: '/' behind_proxy: false web_plugins: - Inventory + - Report::PortUtilization - Report::DuplexMismatch - AdminTask::PseudoDevice - AdminTask::Topology - AdminTask::JobQueue + - AdminTask::Users - Search::Device - Search::Node - Search::VLAN diff --git a/Netdisco/share/public/css/netdisco.css b/Netdisco/share/public/css/netdisco.css index 06a24f5c..997e733b 100644 --- a/Netdisco/share/public/css/netdisco.css +++ b/Netdisco/share/public/css/netdisco.css @@ -28,6 +28,11 @@ body { width: 100%; } +/* results table header should have a background, for floatThead */ +div.content > div.tab-content table.nd_floatinghead thead { + background-color: floralWhite; +} + /* jquery ui autocomplete scrollable */ .ui-autocomplete { max-height: 200px; @@ -70,6 +75,11 @@ body { line-height: 8px; } +/* for where min-width is set but we don't want it */ +.nd_no-min-width { + min-width: 0px; +} + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* styles to adjust the hero box used for homepage + login */ diff --git a/Netdisco/share/public/javascripts/jquery.floatThead.js b/Netdisco/share/public/javascripts/jquery.floatThead.js new file mode 100644 index 00000000..5fccc827 --- /dev/null +++ b/Netdisco/share/public/javascripts/jquery.floatThead.js @@ -0,0 +1,612 @@ +/*! + * jQuery.floatThead + * Copyright (c) 2012 - 2013 Misha Koryak - https://github.com/mkoryak/floatThead + * Licensed under Creative Commons Attribution-NonCommercial 3.0 Unported - http://creativecommons.org/licenses/by-sa/3.0/ + * Date: 8/25/13 + * + * @projectDescription lock a table header in place while scrolling - without breaking styles or events bound to the header + * + * Dependencies: + * jquery 1.9.0 + [required] OR jquery 1.7.0 + jquery UI core + * underscore.js 1.3.0 + [required] + * + * http://notetodogself.blogspot.com + * http://programmingdrunk.com/floatThead/ + * + * Tested on FF13+, Chrome 21+, IE9, IE8 + * + * @author Misha Koryak + * @version 1.0.0 + */ +// ==ClosureCompiler== +// @compilation_level SIMPLE_OPTIMIZATIONS +// @output_file_name jquery.floatThead.min.js +// ==/ClosureCompiler== +/** + * @preserve jQuery.floatThead 1.0.0 + * Copyright (c) 2013 Misha Koryak - https://github.com/mkoryak/floatThead + * Licensed under Creative Commons Attribution-NonCommercial 3.0 Unported - http://creativecommons.org/licenses/by-sa/3.0/ + */ +(function( $ ) { + +//browser stuff +var ieVersion = function(){for(var a=3,b=document.createElement("b"),c=b.all||[];b.innerHTML="",c[0];);return 4"); + $('body').append($table); + var width = $table.find('col').width(); + $table.remove(); + return width == 0; +}; + +/** + * provides a default config object. You can modify this after including this script if you want to change the init defaults + * @type {Object} + */ +$.floatThead = { + defaults: { + cellTag: 'th', + zIndex: 1001, //zindex of the floating thead (actually a container div) + debounceResizeMs: 1, + useAbsolutePositioning: true, //if set to NULL - defaults: has scrollContainer=true, doesnt have scrollContainer=false + scrollingTop: 0, //String or function($table) - offset from top of window where the header should not pass above + //TODO: this got lost somewhere - needs to be re-implemented + scrollingBottom: 0, //String or function($table) - offset from the bottom of the table where the header should stop scrolling + scrollContainer: function($table){ + return $([]); //if the table has horizontal scroll bars then this is the container that has overflow:auto and causes those scroll bars + }, + floatTableClass: 'floatThead-table' + } +}; + +var $window = $(window); +var floatTheadCreated = 0; + + +/** + * debounce and fix window resize event for ie7. ie7 is evil and will fire window resize event when ANY dom element is resized. + * @param debounceMs + * @param cb + */ + +function windowResize(debounceMs, cb){ + var winWidth = $window.width(); + var debouncedCb = _.debounce(function(){ + var winWidthNew = $window.width(); + if(winWidth != winWidthNew){ + winWidth = winWidthNew; + cb(); + } + }, debounceMs); + $window.bind('resize.floatTHead', debouncedCb); +} + +/** + * try to calculate the scrollbar width for your browser/os + * @return {Number} + */ +function scrollbarWidth() { + var $div = $('
') + .css({ width: 100, height: 100, overflow: 'auto', position: 'absolute', top: -1000, left: -1000 }) + .prependTo('body').append('
').find('div') + .css({ width: '100%', height: 200 }); + var scrollbarWidth = 100 - $div.width(); + $div.parent().remove(); + return scrollbarWidth; +} + +/** + * Check if a given table has been datatableized (http://datatables.net) + * @param $table + * @return {Boolean} + */ +function isDatatable($table){ + if($table.dataTableSettings){ + for(var i = 0; i < $table.dataTableSettings.length; i++){ + var table = $table.dataTableSettings[i].nTable; + if($table[0] == table){ + return true; + } + } + } + return false; +} +$.fn.floatThead = function(map){ + if(ieVersion < 8){ + return this; //no more crappy browser support. + } + + isChrome = ifChrome(); //need to call this after dom ready, and now it is. + if(isChrome){ + //because chrome cant read width, these elements are used for sizing the table. Need to create new elements because they must be unstyled by user's css. + document.createElement('fthtr'); //tr + document.createElement('fthtd'); //td + document.createElement('fthfoot'); //tfoot + } + if(_.isString(map)){ + var command = map; + var ret = this; + this.filter('table').each(function(){ + var obj = $(this).data('floatThead-attached'); + if(obj && _.isFunction(obj[command])){ + r = obj[command](); + if(typeof r !== 'undefined'){ + ret = r; + } + } + }); + return ret; + } + var opts = $.extend({}, $.floatThead.defaults, map); + + + this.filter(':not(.'+opts.floatTableClass+')').each(function(){ + var $table = $(this); + if($table.data('floatThead-attached')){ + return true; //continue the each loop + } + if(!$table.is('table')){ + throw new Error('jQuery.floatThead must be run on a table element. ex: $("table").floatThead();'); + } + var $header = $table.find('thead:first'); + var $tbody = $table.find('tbody:first'); + if($header.length == 0){ + throw new Error('jQuery.floatThead must be run on a table that contains a element'); + } + var headerFloated = true; + var scrollingTop, scrollingBottom; + var scrollbarOffset = {vertical: 0, horizontal: 0}; + var scWidth = scrollbarWidth(); + var lastColumnCount = 0; //used by columnNum() + var $scrollContainer = opts.scrollContainer($table) || $([]); //guard against returned nulls + + var useAbsolutePositioning = opts.useAbsolutePositioning; + if(useAbsolutePositioning == null){ //defaults: locked=true, !locked=false + useAbsolutePositioning = opts.scrollContainer($table).length; + } + + var $fthGrp = $(''); + + var locked = $scrollContainer.length > 0; + var wrappedContainer = false; //used with absolute positioning enabled. did we need to wrap the scrollContainer/table with a relative div? + var absoluteToFixedOnScroll = ieVersion && !locked && useAbsolutePositioning; //on ie using absolute positioning doesnt look good with window scrolling, so we change positon to fixed on scroll, and then change it back to absolute when done. + var $floatTable = $(""); + var $floatColGroup = $(""); + var $tableColGroup = $(""); + var $fthRow = $(''); //created unstyled elements + var $floatContainer = $('
'); + var $newHeader = $("
"); + var $sizerRow = $(''); + var $sizerCells = $([]); + var $tableCells = $([]); //used for sizing - either $sizerCells or $tableColGroup cols. $tableColGroup cols are only created in chrome for borderCollapse:collapse because of a chrome bug. + var $headerCells = $([]); + var $fthCells = $([]); //created elements + + $newHeader.append($sizerRow); + $header.detach(); + + $table.prepend($newHeader); + $table.prepend($tableColGroup); + if(isChrome){ + $fthGrp.append($fthRow); + $table.append($fthGrp); + } + + $floatTable.append($floatColGroup); + $floatContainer.append($floatTable); + $floatTable.attr('class', $table.attr('class')); + $floatTable.addClass(opts.floatTableClass).css('margin', 0); //must have no margins or you wont be able to click on things under floating table + + if(useAbsolutePositioning){ + var makeRelative = function($container, alwaysWrap){ + var positionCss = $container.css('position'); + var relativeToScrollContainer = (positionCss == "relative" || positionCss == "absolute"); + if(!relativeToScrollContainer || alwaysWrap){ + var css = {"paddingLeft": $container.css('paddingLeft'), "paddingRight": $container.css('paddingRight')}; + $floatContainer.css(css); + $container = $container.wrap("
").parent(); + wrappedContainer = true; + } + return $container; + }; + if(locked){ + var $relative = makeRelative($scrollContainer, true); + $relative.append($floatContainer); + } else { + makeRelative($table); + $table.after($floatContainer); + } + } else { + $table.after($floatContainer); + } + + + $floatContainer.css({ + position: useAbsolutePositioning ? 'absolute' : 'fixed', + marginTop: 0, + top: useAbsolutePositioning ? 0 : 'auto', + zIndex: opts.zIndex + }); + updateScrollingOffsets(); + + var layoutFixed = {'table-layout': 'fixed'}; + var layoutAuto = {'table-layout': $table.css('tableLayout') || 'auto'}; + + function setHeaderHeight(){ + var headerHeight = $header.outerHeight(true); + $sizerRow.outerHeight(headerHeight); + $sizerCells.outerHeight(headerHeight); + } + + + function setFloatWidth(){ + var tableWidth = $table.outerWidth(); + var width = $scrollContainer.width() || tableWidth; + $floatContainer.width(width - scrollbarOffset.vertical); + if(locked){ + var percent = 100 * tableWidth / (width - scrollbarOffset.vertical); + $floatTable.css('width', percent+'%'); + } else { + $floatTable.outerWidth(tableWidth); + } + } + + function updateScrollingOffsets(){ + scrollingTop = (_.isFunction(opts.scrollingTop) ? opts.scrollingTop($table) : opts.scrollingTop) || 0; + scrollingBottom = (_.isFunction(opts.scrollingBottom) ? opts.scrollingBottom($table) : opts.scrollingBottom) || 0; + } + + /** + * get the number of columns and also rebuild resizer rows if the count is different then the last count + */ + function columnNum(){ + var $headerColumns = $header.find('tr:first>'+opts.cellTag); + + var count = _.reduce($headerColumns, function(sum, cell){ + var colspan = parseInt(($(cell).attr('colspan') || 1), 10); + return sum + colspan; + }, 0); + if(count != lastColumnCount){ + lastColumnCount = count; + var cells = [], cols = [], psuedo = []; + for(var x = 0; x < count; x++){ + cells.push('<'+opts.cellTag+' class="floatThead-col-'+x+'"/>'); + cols.push(''); + psuedo.push(""); + } + + cols = cols.join(''); + cells = cells.join(''); + + if(isChrome){ + psuedo = psuedo.join(''); + $fthRow.html(psuedo); + $fthCells = $fthRow.find('fthcell') + } + + $sizerRow.html(cells); + $tableColGroup.html(cols); + $tableCells = $tableColGroup.find('col'); + $floatColGroup.html(cols); + $headerCells = $floatColGroup.find("col"); + + } + return count; + } + + + function refloat(){ + if(!headerFloated){ + headerFloated = true; + $table.css(layoutFixed); + $floatTable.css(layoutFixed); + $floatTable.append($header); //append because colgroup must go first in chrome + $tbody.before($newHeader); + setHeaderHeight(); + } + } + function unfloat(){ + if(headerFloated){ + headerFloated = false; + $newHeader.detach(); + $table.prepend($header); + $table.css(layoutAuto); + $floatTable.css(layoutAuto); + } + } + function changePositioning(isAbsolute){ + if(useAbsolutePositioning != isAbsolute){ + useAbsolutePositioning = isAbsolute; + $floatContainer.css({ + position: useAbsolutePositioning ? 'absolute' : 'fixed' + }); + } + } + + /** + * returns a function that updates the floating header's cell widths. + * @return {Function} + */ + function reflow(){ + var numCols = columnNum(); //if the tables columns change dynamically since last time (datatables) we need to rebuild the sizer rows and get new count + var flow = function(){ + var badReflow = false; + var $rowCells = isChrome ? $fthCells : $tableCells; + if($rowCells.length == numCols && numCols > 0){ + unfloat(); + for(var i=0; i < numCols; i++){ + var $rowCell = $rowCells.eq(i); + var rowWidth = $rowCell.outerWidth(true); + $headerCells.eq(i).outerWidth(rowWidth); + $tableCells.eq(i).outerWidth(rowWidth); + } + refloat(); + for(var i=0; i < numCols; i++){ + var hw = $headerCells.eq(i).outerWidth(true); + var tw = $tableCells.eq(i).outerWidth(true); + if(hw != tw){ + badReflow = true; + break; + } + } + } else { + $floatTable.append($header); + $table.css(layoutAuto); + $floatTable.css(layoutAuto); + setHeaderHeight(); + } + return badReflow; + }; + return flow; + } + + /** + * first performs initial calculations that we expect to not change when the table, window, or scrolling container are scrolled. + * returns a function that calculates the floating container's top and left coords. takes into account if we are using page scrolling or inner scrolling + * @return {Function} + */ + function calculateFloatContainerPosFn(){ + var scrollingContainerTop = $scrollContainer.scrollTop(); + + //this floatEnd calc was moved out of the returned function because we assume the table height doesnt change (otherwise we must reinit by calling calculateFloatContainerPosFn) + var floatEnd; + var tableContainerGap = 0; + + var floatContainerHeight = $floatContainer.height(); + var tableOffset = $table.offset(); + if(locked){ + var containerOffset = $scrollContainer.offset(); + tableContainerGap = tableOffset.top - containerOffset.top + scrollingContainerTop; + } else { + floatEnd = tableOffset.top - scrollingTop - floatContainerHeight + scrollingBottom + scrollbarOffset.horizontal; + } + var windowTop = $window.scrollTop(); + var windowLeft = $window.scrollLeft(); + var scrollContainerLeft = $scrollContainer.scrollLeft(); + scrollingContainerTop = $scrollContainer.scrollTop(); + + + + var positionFn = function(eventType){ + + if(eventType == 'windowScroll'){ + windowTop = $window.scrollTop(); + windowLeft = $window.scrollLeft(); + } else if(eventType == 'containerScroll'){ + scrollingContainerTop = $scrollContainer.scrollTop(); + scrollContainerLeft = $scrollContainer.scrollLeft(); + } else if(eventType != 'init') { + windowTop = $window.scrollTop(); + windowLeft = $window.scrollLeft(); + scrollingContainerTop = $scrollContainer.scrollTop(); + scrollContainerLeft = $scrollContainer.scrollLeft(); + } + if(absoluteToFixedOnScroll){ + if(eventType == 'windowScrollDone'){ + changePositioning(true); //change to absolute + } else { + changePositioning(false); //change to fixed + } + } else if(eventType == 'windowScrollDone'){ + return null; //event is fired when they stop scrolling. ignore it if not 'absoluteToFixedOnScroll' + } + + tableOffset = $table.offset(); + var top, left, tableHeight; + + //absolute positioning + if(locked && useAbsolutePositioning){ //inner scrolling + if (tableContainerGap >= scrollingContainerTop) { + var gap = tableContainerGap - scrollingContainerTop; + gap = gap > 0 ? gap : 0; + top = gap; + // unfloat(); //more trouble than its worth + } else { + top = wrappedContainer ? 0 : scrollingContainerTop; + // refloat(); //more trouble than its worth + //headers stop at the top of the viewport + } + left = 0; + } else if(!locked && useAbsolutePositioning) { //window scrolling + tableHeight = $table.outerHeight(); + if(windowTop > floatEnd + tableHeight){ + top = tableHeight - floatContainerHeight; //scrolled past table + } else if (tableOffset.top > windowTop + scrollingTop) { + top = 0; //scrolling to table + unfloat(); + } else { + top = scrollingTop + windowTop - tableOffset.top + tableContainerGap; + refloat(); //scrolling within table. header floated + } + left = 0; + + //fixed positioning: + } else if(locked && !useAbsolutePositioning){ //inner scrolling + if (tableContainerGap > scrollingContainerTop) { + top = tableOffset.top - windowTop; + unfloat(); + } else { + top = tableOffset.top + scrollingContainerTop - windowTop - tableContainerGap; + refloat(); + //headers stop at the top of the viewport + } + left = tableOffset.left + scrollContainerLeft - windowLeft; + } else if(!locked && !useAbsolutePositioning) { //window scrolling + tableHeight = $table.outerHeight(); + if(windowTop > floatEnd + tableHeight){ + top = tableHeight + scrollingTop - windowTop + floatEnd; + unfloat(); + } else if (tableOffset.top > windowTop + scrollingTop) { + top = tableOffset.top - windowTop; + refloat(); + } else { + top = scrollingTop; + } + left = tableOffset.left - windowLeft; + } + + return {top: top, left: left}; + }; + return positionFn; + } + /** + * returns a function that caches old floating container position and only updates css when the position changes + * @return {Function} + */ + function repositionFloatContainerFn(){ + var oldTop = null; + var oldLeft = null; + var oldScrollLeft = null; + return function(pos, setWidth, setHeight){ + if(pos != null && (oldTop != pos.top || oldLeft != pos.left)){ + $floatContainer.css({ + top: pos.top, + left: pos.left + }); + oldTop = pos.top; + oldLeft = pos.left; + } + if(setWidth){ + setFloatWidth(); + } + if(setHeight){ + setHeaderHeight(); + } + var scrollLeft = $scrollContainer.scrollLeft(); + if(oldScrollLeft != scrollLeft){ + $floatContainer.scrollLeft(scrollLeft); + oldScrollLeft = scrollLeft; + } + } + } + + /** + * checks if THIS table has scrollbars, and finds their widths + */ + function calculateScrollBarSize(){ //this should happen after the floating table has been positioned + if($scrollContainer.length){ + scrollbarOffset.horizontal = $scrollContainer.width() < $table.width() ? scWidth : 0; + scrollbarOffset.vertical = $scrollContainer.height() < $table.height() ? scWidth: 0; + } + } + //finish up. create all calculation functions and bind them to events + calculateScrollBarSize(); + + var flow = reflow(); + flow(); + var calculateFloatContainerPos = calculateFloatContainerPosFn(); + var repositionFloatContainer = repositionFloatContainerFn(); + + repositionFloatContainer(calculateFloatContainerPos('init'), true, true); //this must come after reflow because reflow changes scrollLeft back to 0 when it rips out the thead + + var windowScrollDoneEvent = _.debounce(function(){ + repositionFloatContainer(calculateFloatContainerPos('windowScrollDone'), false); + }, 300); + + var windowScrollEvent = function(){ + repositionFloatContainer(calculateFloatContainerPos('windowScroll'), false); + windowScrollDoneEvent(); + }; + var containerScrollEvent = function(){ + repositionFloatContainer(calculateFloatContainerPos('containerScroll'), false); + }; + + var windowResizeEvent = function(){ + updateScrollingOffsets(); + calculateScrollBarSize(); + flow = reflow(); + var badReflow = flow(); + if(badReflow){ + flow(); + } + calculateFloatContainerPos = calculateFloatContainerPosFn(); + repositionFloatContainer = repositionFloatContainerFn(); + repositionFloatContainer(calculateFloatContainerPos('resize'), true, true); + }; + var reflowEvent = _.debounce(function(){ + calculateScrollBarSize(); + updateScrollingOffsets(); + flow = reflow(); + var badReflow = flow(); + if(badReflow){ + flow(); + } + calculateFloatContainerPos = calculateFloatContainerPosFn(); + repositionFloatContainer(calculateFloatContainerPos('reflow'), true); + }, 1); + if(locked){ //internal scrolling + if(useAbsolutePositioning){ + $scrollContainer.bind('scroll.floatTHead', containerScrollEvent); + } else { + $scrollContainer.bind('scroll.floatTHead', containerScrollEvent); + $window.bind('scroll.floatTHead', windowScrollEvent); + } + } else { //window scrolling + $window.bind('scroll.floatTHead', windowScrollEvent); + } + + $window.bind('load.floatTHead', reflowEvent); //for tables with images + + windowResize(opts.debounceResizeMs, windowResizeEvent); + $table.bind('reflow', reflowEvent); + if(isDatatable($table)){ + $table + .bind('filter', reflowEvent) + .bind('sort', reflowEvent) + .bind('page', reflowEvent); + } + + //attach some useful functions to the table. + $table.data('floatThead-attached', { + destroy: function(){ + $table.css(layoutAuto); + $tableColGroup.remove(); + $fthGrp.remove(); + $newHeader.replaceWith($header); + $table.unbind('reflow'); + reflowEvent = windowResizeEvent = containerScrollEvent = windowScrollEvent = function() {}; + $scrollContainer.unbind('scroll.floatTHead'); + $floatContainer.remove(); + $table.data('floatThead-attached', false); + floatTheadCreated--; + if(floatTheadCreated == 0){ + $window.unbind('scroll.floatTHead'); + $window.unbind('resize.floatTHead'); + $window.unbind('load.floatTHead'); + } + }, + reflow: function(){ + reflowEvent(); + }, + setHeaderHeight: function(){ + setHeaderHeight(); + }, + getFloatContainer: function(){ + return $floatContainer; + } + }); + floatTheadCreated++; + }); + return this; +}; +})(jQuery); diff --git a/Netdisco/share/public/javascripts/netdisco.js b/Netdisco/share/public/javascripts/netdisco.js index 16bacf37..b55f0dd0 100644 --- a/Netdisco/share/public/javascripts/netdisco.js +++ b/Netdisco/share/public/javascripts/netdisco.js @@ -78,6 +78,10 @@ function do_search (event, tab) { } // delegate to any [device|search] specific JS code + $('div.content > div.tab-content table.nd_floatinghead').floatThead({ + scrollingTop: 40 + ,useAbsolutePositioning: false + }); inner_view_processing(tab); } ); @@ -216,11 +220,21 @@ $(document).ready(function() { $('.nd_sidebar').toggle(250); $('#nd_sidebar-toggle-img-out').toggle(); $('.content').css('margin-right', '10px'); + $('div.content > div.tab-content table.nd_floatinghead').floatThead('destroy'); + $('div.content > div.tab-content table.nd_floatinghead').floatThead({ + scrollingTop: 40 + ,useAbsolutePositioning: false + }); sidebar_hidden = 1; }); $('#nd_sidebar-toggle-img-out').click(function() { $('#nd_sidebar-toggle-img-out').toggle(); $('.content').css('margin-right', '215px'); + $('div.content > div.tab-content table.nd_floatinghead').floatThead('destroy'); + $('div.content > div.tab-content table.nd_floatinghead').floatThead({ + scrollingTop: 40 + ,useAbsolutePositioning: false + }); $('.nd_sidebar').toggle(250); if (! $('.nd_sidebar').hasClass('nd_sidebar-pinned')) { $(window).scrollTop(0); diff --git a/Netdisco/share/public/javascripts/underscore.min.js b/Netdisco/share/public/javascripts/underscore.min.js new file mode 100644 index 00000000..ef9ef9f4 --- /dev/null +++ b/Netdisco/share/public/javascripts/underscore.min.js @@ -0,0 +1,6 @@ +// Underscore.js 1.5.1 +// http://underscorejs.org +// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. +!function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,v=e.reduce,h=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,w=i.bind,j=function(n){return n instanceof j?n:this instanceof j?(this._wrapped=n,void 0):new j(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=j),exports._=j):n._=j,j.VERSION="1.5.1";var A=j.each=j.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(j.has(n,a)&&t.call(e,n[a],a,n)===r)return};j.map=j.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e.push(t.call(r,n,u,i))}),e)};var E="Reduce of empty array with no initial value";j.reduce=j.foldl=j.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduce===v)return e&&(t=j.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(E);return r},j.reduceRight=j.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduceRight===h)return e&&(t=j.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=j.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(E);return r},j.find=j.detect=function(n,t,r){var e;return O(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},j.filter=j.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&e.push(n)}),e)},j.reject=function(n,t,r){return j.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},j.every=j.all=function(n,t,e){t||(t=j.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var O=j.some=j.any=function(n,t,e){t||(t=j.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};j.contains=j.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:O(n,function(n){return n===t})},j.invoke=function(n,t){var r=o.call(arguments,2),e=j.isFunction(t);return j.map(n,function(n){return(e?t:n[t]).apply(n,r)})},j.pluck=function(n,t){return j.map(n,function(n){return n[t]})},j.where=function(n,t,r){return j.isEmpty(t)?r?void 0:[]:j[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},j.findWhere=function(n,t){return j.where(n,t,!0)},j.max=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.max.apply(Math,n);if(!t&&j.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>e.computed&&(e={value:n,computed:a})}),e.value},j.min=function(n,t,r){if(!t&&j.isArray(n)&&n[0]===+n[0]&&n.length<65535)return Math.min.apply(Math,n);if(!t&&j.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;ae||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.indexi;){var o=i+a>>>1;r.call(e,n[o])=0})})},j.difference=function(n){var t=c.apply(e,o.call(arguments,1));return j.filter(n,function(n){return!j.contains(t,n)})},j.zip=function(){for(var n=j.max(j.pluck(arguments,"length").concat(0)),t=new Array(n),r=0;n>r;r++)t[r]=j.pluck(arguments,""+r);return t},j.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},j.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=j.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},j.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},j.range=function(n,t,r){arguments.length<=1&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=new Array(e);e>u;)i[u++]=n,n+=r;return i};var M=function(){};j.bind=function(n,t){var r,e;if(w&&n.bind===w)return w.apply(n,o.call(arguments,1));if(!j.isFunction(n))throw new TypeError;return r=o.call(arguments,2),e=function(){if(!(this instanceof e))return n.apply(t,r.concat(o.call(arguments)));M.prototype=n.prototype;var u=new M;M.prototype=null;var i=n.apply(u,r.concat(o.call(arguments)));return Object(i)===i?i:u}},j.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},j.bindAll=function(n){var t=o.call(arguments,1);if(0===t.length)throw new Error("bindAll must be passed function names");return A(t,function(t){n[t]=j.bind(n[t],n)}),n},j.memoize=function(n,t){var r={};return t||(t=j.identity),function(){var e=t.apply(this,arguments);return j.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},j.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},j.defer=function(n){return j.delay.apply(j,[n,1].concat(o.call(arguments,1)))},j.throttle=function(n,t,r){var e,u,i,a=null,o=0;r||(r={});var c=function(){o=r.leading===!1?0:new Date,a=null,i=n.apply(e,u)};return function(){var l=new Date;o||r.leading!==!1||(o=l);var f=t-(l-o);return e=this,u=arguments,0>=f?(clearTimeout(a),a=null,o=l,i=n.apply(e,u)):a||r.trailing===!1||(a=setTimeout(c,f)),i}},j.debounce=function(n,t,r){var e,u=null;return function(){var i=this,a=arguments,o=function(){u=null,r||(e=n.apply(i,a))},c=r&&!u;return clearTimeout(u),u=setTimeout(o,t),c&&(e=n.apply(i,a)),e}},j.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},j.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},j.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},j.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},j.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)j.has(n,r)&&t.push(r);return t},j.values=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push(n[r]);return t},j.pairs=function(n){var t=[];for(var r in n)j.has(n,r)&&t.push([r,n[r]]);return t},j.invert=function(n){var t={};for(var r in n)j.has(n,r)&&(t[n[r]]=r);return t},j.functions=j.methods=function(n){var t=[];for(var r in n)j.isFunction(n[r])&&t.push(r);return t.sort()},j.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},j.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},j.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)j.contains(r,u)||(t[u]=n[u]);return t},j.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]===void 0&&(n[r]=t[r])}),n},j.clone=function(n){return j.isObject(n)?j.isArray(n)?n.slice():j.extend({},n):n},j.tap=function(n,t){return t(n),n};var S=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof j&&(n=n._wrapped),t instanceof j&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==String(t);case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;var a=n.constructor,o=t.constructor;if(a!==o&&!(j.isFunction(a)&&a instanceof a&&j.isFunction(o)&&o instanceof o))return!1;r.push(n),e.push(t);var c=0,f=!0;if("[object Array]"==u){if(c=n.length,f=c==t.length)for(;c--&&(f=S(n[c],t[c],r,e)););}else{for(var s in n)if(j.has(n,s)&&(c++,!(f=j.has(t,s)&&S(n[s],t[s],r,e))))break;if(f){for(s in t)if(j.has(t,s)&&!c--)break;f=!c}}return r.pop(),e.pop(),f};j.isEqual=function(n,t){return S(n,t,[],[])},j.isEmpty=function(n){if(null==n)return!0;if(j.isArray(n)||j.isString(n))return 0===n.length;for(var t in n)if(j.has(n,t))return!1;return!0},j.isElement=function(n){return!(!n||1!==n.nodeType)},j.isArray=x||function(n){return"[object Array]"==l.call(n)},j.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){j["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),j.isArguments(arguments)||(j.isArguments=function(n){return!(!n||!j.has(n,"callee"))}),"function"!=typeof/./&&(j.isFunction=function(n){return"function"==typeof n}),j.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},j.isNaN=function(n){return j.isNumber(n)&&n!=+n},j.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},j.isNull=function(n){return null===n},j.isUndefined=function(n){return n===void 0},j.has=function(n,t){return f.call(n,t)},j.noConflict=function(){return n._=t,this},j.identity=function(n){return n},j.times=function(n,t,r){for(var e=Array(Math.max(0,n)),u=0;n>u;u++)e[u]=t.call(r,u);return e},j.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var I={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};I.unescape=j.invert(I.escape);var T={escape:new RegExp("["+j.keys(I.escape).join("")+"]","g"),unescape:new RegExp("("+j.keys(I.unescape).join("|")+")","g")};j.each(["escape","unescape"],function(n){j[n]=function(t){return null==t?"":(""+t).replace(T[n],function(t){return I[n][t]})}}),j.result=function(n,t){if(null==n)return void 0;var r=n[t];return j.isFunction(r)?r.call(n):r},j.mixin=function(n){A(j.functions(n),function(t){var r=j[t]=n[t];j.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),z.call(this,r.apply(j,n))}})};var N=0;j.uniqueId=function(n){var t=++N+"";return n?n+t:t},j.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var q=/(.)^/,B={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\t|\u2028|\u2029/g;j.template=function(n,t,r){var e;r=j.defaults({},r,j.templateSettings);var u=new RegExp([(r.escape||q).source,(r.interpolate||q).source,(r.evaluate||q).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(D,function(n){return"\\"+B[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=new Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,j);var c=function(n){return e.call(this,n,j)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},j.chain=function(n){return j(n).chain()};var z=function(n){return this._chain?j(n).chain():n};j.mixin(j),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];j.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],z.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];j.prototype[n]=function(){return z.call(this,t.apply(this._wrapped,arguments))}}),j.extend(j.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}.call(this); +//# sourceMappingURL=underscore-min.map \ No newline at end of file diff --git a/Netdisco/share/views/admintask.tt b/Netdisco/share/views/admintask.tt index 8963e10d..aabc01a7 100644 --- a/Netdisco/share/views/admintask.tt +++ b/Netdisco/share/views/admintask.tt @@ -32,7 +32,7 @@ - + [% END %] diff --git a/Netdisco/share/views/ajax/admintask/jobqueue.tt b/Netdisco/share/views/ajax/admintask/jobqueue.tt index 479d5793..7d07a491 100644 --- a/Netdisco/share/views/ajax/admintask/jobqueue.tt +++ b/Netdisco/share/views/ajax/admintask/jobqueue.tt @@ -1,4 +1,7 @@ -
+[% IF results.count == 0 %] +
The job queue is empty.
+[% ELSE %] +
@@ -10,7 +13,21 @@ - + @@ -47,4 +64,5 @@ [% END %]
EnteredUser Started FinishedAction +
+ + +
+
+[% END %] diff --git a/Netdisco/share/views/ajax/admintask/pseudodevice.tt b/Netdisco/share/views/ajax/admintask/pseudodevice.tt index 4ed632d2..8678e256 100644 --- a/Netdisco/share/views/ajax/admintask/pseudodevice.tt +++ b/Netdisco/share/views/ajax/admintask/pseudodevice.tt @@ -1,4 +1,4 @@ - +
diff --git a/Netdisco/share/views/ajax/admintask/topology.tt b/Netdisco/share/views/ajax/admintask/topology.tt index d05275ea..a21cb51b 100644 --- a/Netdisco/share/views/ajax/admintask/topology.tt +++ b/Netdisco/share/views/ajax/admintask/topology.tt @@ -1,4 +1,4 @@ -
Device Name
+
diff --git a/Netdisco/share/views/ajax/admintask/users.tt b/Netdisco/share/views/ajax/admintask/users.tt new file mode 100644 index 00000000..faff2000 --- /dev/null +++ b/Netdisco/share/views/ajax/admintask/users.tt @@ -0,0 +1,52 @@ +
Left Device
+ + + + + + + + + + + + + + + + + + + + + + [% WHILE (row = results.next) %] + + + + + + + + + + [% END %] + +
Full NameUsernamePasswordPort ControlAdministratorAction
+ +
+ + + + + + + + + + + + + +
+ diff --git a/Netdisco/share/views/ajax/device/addresses.tt b/Netdisco/share/views/ajax/device/addresses.tt index 30805b23..36231a3c 100644 --- a/Netdisco/share/views/ajax/device/addresses.tt +++ b/Netdisco/share/views/ajax/device/addresses.tt @@ -1,4 +1,4 @@ - +
diff --git a/Netdisco/share/views/ajax/device/ports.tt b/Netdisco/share/views/ajax/device/ports.tt index 5cb6c418..c3d9bc68 100644 --- a/Netdisco/share/views/ajax/device/ports.tt +++ b/Netdisco/share/views/ajax/device/ports.tt @@ -1,4 +1,4 @@ -
Address
+
diff --git a/Netdisco/share/views/ajax/report/duplexmismatch.tt b/Netdisco/share/views/ajax/report/duplexmismatch.tt index a13f40be..f9632348 100644 --- a/Netdisco/share/views/ajax/report/duplexmismatch.tt +++ b/Netdisco/share/views/ajax/report/duplexmismatch.tt @@ -1,4 +1,4 @@ -
+
@@ -12,7 +12,7 @@ [% WHILE (row = results.next) %] - diff --git a/Netdisco/share/views/ajax/report/portutilization.tt b/Netdisco/share/views/ajax/report/portutilization.tt new file mode 100644 index 00000000..3c981df0 --- /dev/null +++ b/Netdisco/share/views/ajax/report/portutilization.tt @@ -0,0 +1,23 @@ +
Left Device
[% row.left_dns || row.left_ip | html_entity %] + [% row.left_dns || row.left_ip | html_entity %] [% row.left_port | html_entity %]
+ + + + + + + + + + + [% WHILE (row = results.next) %] + + + + + + + + [% END %] + +
DeviceTotal PortsIn UseShutdownFree
[% row.dns || row.ip | html_entity %][% row.port_count %][% row.ports_in_use %][% row.ports_shutdown %][% row.ports_free %]
+ diff --git a/Netdisco/share/views/ajax/search/device.tt b/Netdisco/share/views/ajax/search/device.tt index dd24dc64..8ee38d10 100644 --- a/Netdisco/share/views/ajax/search/device.tt +++ b/Netdisco/share/views/ajax/search/device.tt @@ -1,4 +1,4 @@ - +
diff --git a/Netdisco/share/views/ajax/search/node_by_ip.tt b/Netdisco/share/views/ajax/search/node_by_ip.tt index 28ba2680..04122eaf 100644 --- a/Netdisco/share/views/ajax/search/node_by_ip.tt +++ b/Netdisco/share/views/ajax/search/node_by_ip.tt @@ -1,4 +1,4 @@ -
Device
+
diff --git a/Netdisco/share/views/ajax/search/node_by_mac.tt b/Netdisco/share/views/ajax/search/node_by_mac.tt index adefc4c8..4206dea7 100644 --- a/Netdisco/share/views/ajax/search/node_by_mac.tt +++ b/Netdisco/share/views/ajax/search/node_by_mac.tt @@ -1,4 +1,4 @@ -
MAC
+
diff --git a/Netdisco/share/views/ajax/search/port.tt b/Netdisco/share/views/ajax/search/port.tt index 4bded247..7d00832f 100644 --- a/Netdisco/share/views/ajax/search/port.tt +++ b/Netdisco/share/views/ajax/search/port.tt @@ -1,4 +1,4 @@ -
MAC
+
diff --git a/Netdisco/share/views/ajax/search/vlan.tt b/Netdisco/share/views/ajax/search/vlan.tt index 38056085..8b128770 100644 --- a/Netdisco/share/views/ajax/search/vlan.tt +++ b/Netdisco/share/views/ajax/search/vlan.tt @@ -1,4 +1,4 @@ -
Description
+
diff --git a/Netdisco/share/views/js/admintask.js b/Netdisco/share/views/js/admintask.js index 813a3d2c..02737eb9 100644 --- a/Netdisco/share/views/js/admintask.js +++ b/Netdisco/share/views/js/admintask.js @@ -12,7 +12,7 @@ // reload this table every 5 seconds if (tab == 'jobqueue' - && $('#nd_countdown-control-icon').hasClass('icon-pause')) { + && $('#nd_countdown-control-icon').hasClass('icon-play')) { $('#nd_countdown').text('5'); nd_timers.push(setTimeout(function() { $('#nd_countdown').text('4') }, 1000 )); @@ -94,7 +94,7 @@ var icon = $('#nd_countdown-control-icon'); icon.toggleClass('icon-pause icon-play text-error text-success'); - if (icon.hasClass('icon-play')) { + if (icon.hasClass('icon-pause')) { for (var i = 0; i < nd_timers.length; i++) { clearTimeout(nd_timers[i]); } diff --git a/Netdisco/share/views/layouts/main.tt b/Netdisco/share/views/layouts/main.tt index a0a8461a..9f752fd4 100644 --- a/Netdisco/share/views/layouts/main.tt +++ b/Netdisco/share/views/layouts/main.tt @@ -17,9 +17,11 @@ + +
Vlan