From a2bba4484d6f703ccdb08e74439df481687e0b8b Mon Sep 17 00:00:00 2001 From: "Eric A. Miller" Date: Tue, 31 Dec 2013 13:53:49 -0500 Subject: [PATCH] Squashed commit of the following: commit 49e585ef4bcbafd1964f0d0b8b9eda4b2624c86e Author: Oliver Gorwits Date: Mon Dec 30 23:20:05 2013 +0000 default is to have age filter enabled commit c3635b3b096ea73906a553db5b588eaee29b2145 Author: Oliver Gorwits Date: Mon Dec 30 23:15:08 2013 +0000 disable Seen IPs checkbox when v6ish cidr is seen commit f1550f2dcff96a9b2d77cbc3a8582429f3b078df Author: Oliver Gorwits Date: Mon Dec 30 22:56:12 2013 +0000 make daterange green when age filter activated commit 838578f2b9935c6f2abba7679f2ba87f30f651a3 Author: Oliver Gorwits Date: Mon Dec 30 22:47:29 2013 +0000 use class to control input colouring commit 291d230e6a0216b4f5ad52b8eb04f3949ffe85e3 Author: Oliver Gorwits Date: Mon Dec 30 22:43:45 2013 +0000 move cidr input and remove sidebar title this is only so that the tooltip on the cide input isn't obscured by the table heading. I've no idea how to avoid that happening :-/ commit d0df5c7af82bea3fc674585962ca67f4cd82e553 Author: Oliver Gorwits Date: Mon Dec 30 22:11:34 2013 +0000 layout fixes for ipinventory sidebar commit faeb614147e673d4d08822cafb65ca003c400edd Author: Oliver Gorwits Date: Mon Dec 30 21:27:01 2013 +0000 prevent autoload for ipinventory data; prevent submit if CIDR is empty commit b993611ff9bf1bd1eaed410beab09c754c786b1a Author: Oliver Gorwits Date: Mon Dec 30 21:08:26 2013 +0000 revert to 0.0.0.0/32 default search commit d9e34ec19a2ca0d5893215a0508478985c96076e Author: Oliver Gorwits Date: Mon Dec 30 20:59:22 2013 +0000 set age sort to desc - show oldest unused commit 4a4231c69c8483c65d211119d772ad40a3a95b69 Author: Oliver Gorwits Date: Mon Dec 30 20:55:00 2013 +0000 fix parameter names; make 0.0.0.0/0 default commit 5fb6e42426ae8bf96d81b2f2ad25d118c900646d Author: Eric A. Miller Date: Mon Dec 30 15:16:13 2013 -0500 Add IP Inventory report with options sidebar commit 4b84ca3714990199303cd5e099310e4c1ff1458f Author: Eric A. Miller Date: Mon Dec 30 14:34:29 2013 -0500 Add 'IP' as a report category commit 849ae4e0a6f51102cb90236629f4e1b43fc9bc2e Author: Eric A. Miller Date: Mon Dec 30 14:33:10 2013 -0500 Add CidrIps virtual view ResultSource. Enumerates all IP addresses within an IPv4 subnet. commit 30abbb957bd40270a14ca6289e76f70bf721696e Author: Eric A. Miller Date: Mon Dec 30 14:29:27 2013 -0500 Add Dan Grossman's date range picker component for Twitter Bootstrap. Component relies on Moment.js which is also added. commit 19830639bdbcd409a49228f18d29f6dff5e933e8 Author: Eric A. Miller Date: Mon Dec 30 14:23:11 2013 -0500 DBIx::Class::Helpers is currently a requirement for DBIx::Class::ResultSource::View's which use Helper::Row::SubClass. Use a base ResultSet class so that we can make Helper::ResultSet::SetOperations and Helper::ResultSet::Shortcut available to all of our classes. SetOperations is specifically needed for UNION operations, Shortcut's are candy. Note: Trying to add entire Helper::ResultSet seemed to cause problems. --- Netdisco/lib/App/Netdisco/DB.pm | 4 +- .../App/Netdisco/DB/Result/Virtual/CidrIps.pm | 45 + Netdisco/lib/App/Netdisco/DB/ResultSet.pm | 11 + .../lib/App/Netdisco/DB/ResultSet/Admin.pm | 2 +- .../lib/App/Netdisco/DB/ResultSet/Device.pm | 2 +- .../App/Netdisco/DB/ResultSet/DevicePort.pm | 2 +- .../lib/App/Netdisco/DB/ResultSet/Node.pm | 2 +- .../lib/App/Netdisco/DB/ResultSet/NodeIp.pm | 2 +- .../App/Netdisco/DB/ResultSet/NodeWireless.pm | 2 +- .../lib/App/Netdisco/DB/ResultSet/Subnet.pm | 2 +- .../App/Netdisco/Manual/WritingPlugins.pod | 4 + Netdisco/lib/App/Netdisco/Web/Plugin.pm | 2 +- .../Netdisco/Web/Plugin/Report/IpInventory.pm | 169 ++++ Netdisco/lib/App/Netdisco/Web/TypeAhead.pm | 11 + Netdisco/share/config.yml | 1 + .../share/public/css/daterangepicker-bs2.css | 235 +++++ Netdisco/share/public/css/netdisco.css | 8 + .../share/views/ajax/report/ipinventory.tt | 39 + .../views/ajax/report/ipinventory_csv.tt | 14 + Netdisco/share/views/js/common.js | 2 + Netdisco/share/views/js/daterangepicker.js | 876 ++++++++++++++++++ Netdisco/share/views/js/moment.min.js | 6 + Netdisco/share/views/js/report.js | 49 +- Netdisco/share/views/layouts/main.tt | 1 + .../share/views/sidebar/report/ipinventory.tt | 98 ++ 25 files changed, 1578 insertions(+), 11 deletions(-) create mode 100644 Netdisco/lib/App/Netdisco/DB/Result/Virtual/CidrIps.pm create mode 100644 Netdisco/lib/App/Netdisco/DB/ResultSet.pm create mode 100644 Netdisco/lib/App/Netdisco/Web/Plugin/Report/IpInventory.pm create mode 100644 Netdisco/share/public/css/daterangepicker-bs2.css create mode 100644 Netdisco/share/views/ajax/report/ipinventory.tt create mode 100644 Netdisco/share/views/ajax/report/ipinventory_csv.tt create mode 100644 Netdisco/share/views/js/daterangepicker.js create mode 100644 Netdisco/share/views/js/moment.min.js create mode 100644 Netdisco/share/views/sidebar/report/ipinventory.tt diff --git a/Netdisco/lib/App/Netdisco/DB.pm b/Netdisco/lib/App/Netdisco/DB.pm index abbc8377..6bfaf812 100644 --- a/Netdisco/lib/App/Netdisco/DB.pm +++ b/Netdisco/lib/App/Netdisco/DB.pm @@ -6,7 +6,9 @@ use warnings; use base 'DBIx::Class::Schema'; -__PACKAGE__->load_namespaces; +__PACKAGE__->load_namespaces( + default_resultset_class => 'ResultSet', +); our $VERSION = 31; # schema version used for upgrades, keep as integer diff --git a/Netdisco/lib/App/Netdisco/DB/Result/Virtual/CidrIps.pm b/Netdisco/lib/App/Netdisco/DB/Result/Virtual/CidrIps.pm new file mode 100644 index 00000000..6f7783d3 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/Result/Virtual/CidrIps.pm @@ -0,0 +1,45 @@ +package App::Netdisco::DB::Result::Virtual::CidrIps; + +use strict; +use warnings; + +use utf8; +use base 'DBIx::Class::Core'; + +__PACKAGE__->table_class('DBIx::Class::ResultSource::View'); + +__PACKAGE__->table('cidr_ips'); +__PACKAGE__->result_source_instance->is_virtual(1); +__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL'); +SELECT host(network (cidr) + sub.int)::inet AS ip, NULL::text AS dns, + NULL::timestamp AS time_first, NULL::timestamp AS time_last, false::boolean AS active +FROM + ( SELECT cidr, generate_series(1, broadcast(cidr) - (network(cidr)) - 1) AS int +FROM ( +SELECT CASE WHEN family(cidr) = 4 THEN cidr + ELSE '0.0.0.0/32'::inet + END AS cidr +FROM ( SELECT ?::inet AS cidr) AS input) AS addr +) AS sub +ENDSQL + +__PACKAGE__->add_columns( + "ip", + { data_type => "inet", is_nullable => 0 }, + "dns", + { data_type => "text", is_nullable => 1 }, + "active", + { data_type => "boolean", is_nullable => 1 }, + "time_first", + { + data_type => "timestamp", + is_nullable => 1, + }, + "time_last", + { + data_type => "timestamp", + is_nullable => 1, + }, +); + +1; \ No newline at end of file diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet.pm new file mode 100644 index 00000000..c9272050 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet.pm @@ -0,0 +1,11 @@ +package App::Netdisco::DB::ResultSet; + +use strict; +use warnings; + +use base 'DBIx::Class::ResultSet'; + +__PACKAGE__->load_components( + qw{Helper::ResultSet::SetOperations Helper::ResultSet::Shortcut}); + +1; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm index 2777454b..30d7a4fe 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm @@ -1,5 +1,5 @@ package App::Netdisco::DB::ResultSet::Admin; -use base 'DBIx::Class::ResultSet'; +use base 'App::Netdisco::DB::ResultSet'; use strict; use warnings FATAL => 'all'; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm index bcfee23b..25af6988 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm @@ -1,5 +1,5 @@ package App::Netdisco::DB::ResultSet::Device; -use base 'DBIx::Class::ResultSet'; +use base 'App::Netdisco::DB::ResultSet'; use strict; use warnings FATAL => 'all'; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/DevicePort.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/DevicePort.pm index 29d3fa56..1bb18f49 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/DevicePort.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/DevicePort.pm @@ -1,5 +1,5 @@ package App::Netdisco::DB::ResultSet::DevicePort; -use base 'DBIx::Class::ResultSet'; +use base 'App::Netdisco::DB::ResultSet'; use strict; use warnings FATAL => 'all'; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Node.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Node.pm index dc031e60..39fad973 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/Node.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Node.pm @@ -1,5 +1,5 @@ package App::Netdisco::DB::ResultSet::Node; -use base 'DBIx::Class::ResultSet'; +use base 'App::Netdisco::DB::ResultSet'; use strict; use warnings FATAL => 'all'; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeIp.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeIp.pm index f99d0f4f..c36f80f1 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeIp.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeIp.pm @@ -1,5 +1,5 @@ package App::Netdisco::DB::ResultSet::NodeIp; -use base 'DBIx::Class::ResultSet'; +use base 'App::Netdisco::DB::ResultSet'; use strict; use warnings FATAL => 'all'; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm index a86cf712..a4288ebf 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm @@ -1,5 +1,5 @@ package App::Netdisco::DB::ResultSet::NodeWireless; -use base 'DBIx::Class::ResultSet'; +use base 'App::Netdisco::DB::ResultSet'; use strict; use warnings FATAL => 'all'; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm index 75e85a2e..d604382c 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm @@ -1,5 +1,5 @@ package App::Netdisco::DB::ResultSet::Subnet; -use base 'DBIx::Class::ResultSet'; +use base 'App::Netdisco::DB::ResultSet'; use strict; use warnings FATAL => 'all'; diff --git a/Netdisco/lib/App/Netdisco/Manual/WritingPlugins.pod b/Netdisco/lib/App/Netdisco/Manual/WritingPlugins.pod index 5fa90d92..e7ab0848 100644 --- a/Netdisco/lib/App/Netdisco/Manual/WritingPlugins.pod +++ b/Netdisco/lib/App/Netdisco/Manual/WritingPlugins.pod @@ -137,6 +137,10 @@ Port =item * +IP + +=item * + Node =item * diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin.pm b/Netdisco/lib/App/Netdisco/Web/Plugin.pm index d010bf57..0604914b 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin.pm @@ -16,7 +16,7 @@ set( '_admin_tasks' => {}, '_reports_menu' => {}, '_reports' => {}, - '_report_order' => [qw/Device Port Node VLAN Network Wireless/], + '_report_order' => [qw/Device Port IP Node VLAN Network Wireless/], ); # this is what Dancer::Template::TemplateToolkit does by default diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Report/IpInventory.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Report/IpInventory.pm new file mode 100644 index 00000000..6868e6ea --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Report/IpInventory.pm @@ -0,0 +1,169 @@ +package App::Netdisco::Web::Plugin::Report::IpInventory; + +use Dancer ':syntax'; +use Dancer::Plugin::DBIC; +use Dancer::Plugin::Auth::Extensible; + +use App::Netdisco::Web::Plugin; + +register_report( + { category => 'IP', + tag => 'ipinventory', + label => 'IP Inventory', + provides_csv => 1, + } +); + +# Following two Perl Core 5.10+ +use Time::Piece; +use Time::Seconds; + +hook 'before' => sub { + + return + unless ( request->path eq uri_for('/report/ipinventory')->path + or index( request->path, uri_for('/ajax/content/report/ipinventory')->path ) + == 0 ); + + my $start = Time::Piece->new - ONE_DAY * 29; + + params->{'limit'} ||= 256; + params->{'daterange'} + ||= $start->ymd . " to " . Time::Piece->new->ymd; +}; + +get '/ajax/content/report/ipinventory' => require_login sub { + + # Default to something simple with no results to prevent + # "Search failed!" error + my $subnet = param('subnet') || '0.0.0.0/32'; + + my $age = param('age_on') || '0'; + my $agenot = param('age_invert') || '0'; + + my ( $start, $end ) = param('daterange') =~ /(\d+-\d+-\d+)/gmx; + + my $limit = param('limit') || 256; + my $order = param('order') || 'IP'; + my $never = param('never') || '0'; + + # We need a reasonable limit to prevent a potential DoS, especially if + # 'never' is true. TODO: Need better input validation, both JS and + # server-side to provide user feedback + $limit = 8192 if $limit > 8192; + $order = $order eq 'IP' ? \'ip ASC' : \'age DESC'; + + my $rs1 = schema('netdisco')->resultset('DeviceIp')->search( + undef, + { join => 'device', + select => [ + 'alias AS ip', + 'creation AS time_first', + 'device.last_discover AS time_last', + 'dns', + \'true AS active', + \'false AS node', + \'age(device.last_discover) AS age' + ], + as => [qw( ip time_first time_last dns active node age)], + } + )->hri; + + my $rs2 = schema('netdisco')->resultset('NodeIp')->search( + undef, + { columns => [qw( ip time_first time_last dns active)], + '+select' => [ \'true AS node', \'age(time_last) AS age' ], + '+as' => [ 'node', 'age' ], + } + )->hri; + + my $rs3 = schema('netdisco')->resultset('NodeNbt')->search( + undef, + { columns => [qw( ip time_first time_last )], + '+select' => [ + 'nbname AS dns', 'active', + \'true AS node', \'age(time_last) AS age' + ], + '+as' => [ 'dns', 'active', 'node', 'age' ], + } + )->hri; + + my $rs_union = $rs1->union( [ $rs2, $rs3 ] ); + + if ( $never ) { + my $rs4 = schema('netdisco')->resultset('Virtual::CidrIps')->search( + undef, + { bind => [ $subnet ], + columns => [qw( ip time_first time_last dns active)], + '+select' => [ \'false AS node', \'age(time_last) AS age' ], + '+as' => [ 'node', 'age' ], + } + )->hri; + + $rs_union = $rs_union->union( [$rs4] ); + } + + my $rs_sub = $rs_union->search( + { ip => { '<<' => $subnet } }, + { order_by => [qw( ip time_last )], + select => [ + \'DISTINCT ON (ip) ip', + 'dns', + \'date_trunc(\'second\', time_last) AS time_last', + \'date_trunc(\'second\', time_first) AS time_first', + 'active', + 'node', + 'age' + ], + as => [ + 'ip', 'dns', 'time_last', 'time_first', + 'active', 'node', 'age' + ], + } + )->as_query; + + my $rs; + if ( $age && $start && $end ) { + $start = $start . ' 00:00:00'; + $end = $end . ' 23:59:59'; + + if ( $agenot ) { + $rs = $rs_union->search( + { -or => [ + time_first => [ { '<', $start }, undef ], + time_last => { '>', $end }, + ] + }, + { from => { me => $rs_sub }, } + ); + } + else { + $rs = $rs_union->search( + { -and => [ + time_first => { '>=', $start }, + time_last => { '<=', $end }, + ] + }, + { from => { me => $rs_sub }, } + ); + } + } + else { + $rs = $rs_union->search( undef, { from => { me => $rs_sub }, } ); + } + + my @results = $rs->order_by($order)->limit($limit)->all; + return unless scalar @results; + + if ( request->is_ajax ) { + template 'ajax/report/ipinventory.tt', { results => \@results, }, + { layout => undef }; + } + else { + header( 'Content-Type' => 'text/comma-separated-values' ); + template 'ajax/report/ipinventory_csv.tt', { results => \@results, }, + { layout => undef }; + } +}; + +1; diff --git a/Netdisco/lib/App/Netdisco/Web/TypeAhead.pm b/Netdisco/lib/App/Netdisco/Web/TypeAhead.pm index f10fc415..16780f88 100644 --- a/Netdisco/lib/App/Netdisco/Web/TypeAhead.pm +++ b/Netdisco/lib/App/Netdisco/Web/TypeAhead.pm @@ -55,4 +55,15 @@ ajax '/ajax/data/port/typeahead' => require_login sub { to_json \@$results; }; +ajax '/ajax/data/subnet/typeahead' => require_login sub { + my $q = param('query') || param('term'); + $q = "$q\%" if $q !~ m/\%/; + my $nets = schema('netdisco')->resultset('Subnet')->search( + { 'me.net::text' => { '-ilike' => $q }}, + { columns => ['net'] } ); + + content_type 'application/json'; + to_json [map {$_->net} $nets->all]; +}; + true; diff --git a/Netdisco/share/config.yml b/Netdisco/share/config.yml index e605ae8a..db8dae23 100644 --- a/Netdisco/share/config.yml +++ b/Netdisco/share/config.yml @@ -45,6 +45,7 @@ web_plugins: - Report::DeviceByLocation - Report::DevicePoeStatus - Report::DuplexMismatch + - Report::IpInventory - Report::NodeMultiIPs - Report::PhonesDiscovered - Report::SsidInventory diff --git a/Netdisco/share/public/css/daterangepicker-bs2.css b/Netdisco/share/public/css/daterangepicker-bs2.css new file mode 100644 index 00000000..a36fb08a --- /dev/null +++ b/Netdisco/share/public/css/daterangepicker-bs2.css @@ -0,0 +1,235 @@ +/*! + * Stylesheet for the Date Range Picker, for use with Bootstrap 2.x + * + * Copyright 2013 Dan Grossman ( http://www.dangrossman.info ) + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Built for http://www.improvely.com + */ + +.daterangepicker.dropdown-menu { + max-width: none; + z-index: 3000; +} + +.daterangepicker.opensleft .ranges, .daterangepicker.opensleft .calendar { + float: left; + margin: 4px; +} + +.daterangepicker.opensright .ranges, .daterangepicker.opensright .calendar { + float: right; + margin: 4px; +} + +.daterangepicker .ranges { + width: 160px; + text-align: left; +} + +.daterangepicker .ranges .range_inputs>div { + float: left; +} + +.daterangepicker .ranges .range_inputs>div:nth-child(2) { + padding-left: 11px; +} + +.daterangepicker .calendar { + display: none; + max-width: 250px; +} + +.daterangepicker .calendar th, .daterangepicker .calendar td { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + white-space: nowrap; + text-align: center; +} + +.daterangepicker .ranges label { + color: #333; + font-size: 11px; + margin-bottom: 2px; + text-transform: uppercase; + text-shadow: 1px 1px 0 #fff; +} + +.daterangepicker .ranges input { + font-size: 11px; +} + +.daterangepicker .ranges ul { + list-style: none; + margin: 0; + padding: 0; +} + +.daterangepicker .ranges li { + font-size: 13px; + background: #f5f5f5; + border: 1px solid #f5f5f5; + color: #08c; + padding: 3px 12px; + margin-bottom: 8px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + cursor: pointer; +} + +.daterangepicker .ranges li.active, .daterangepicker .ranges li:hover { + background: #08c; + border: 1px solid #08c; + color: #fff; +} + +.daterangepicker .calendar-date { + border: 1px solid #ddd; + padding: 4px; + border-radius: 4px; + background: #fff; +} + +.daterangepicker .calendar-time { + text-align: center; + margin: 8px auto 0 auto; + line-height: 30px; +} + +.daterangepicker { + position: absolute; + background: #fff; + top: 100px; + left: 20px; + padding: 4px; + margin-top: 1px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + +.daterangepicker.opensleft:before { + position: absolute; + top: -7px; + right: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.daterangepicker.opensleft:after { + position: absolute; + top: -6px; + right: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; + content: ''; +} + +.daterangepicker.opensright:before { + position: absolute; + top: -7px; + left: 9px; + display: inline-block; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-left: 7px solid transparent; + border-bottom-color: rgba(0, 0, 0, 0.2); + content: ''; +} + +.daterangepicker.opensright:after { + position: absolute; + top: -6px; + left: 10px; + display: inline-block; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + border-left: 6px solid transparent; + content: ''; +} + +.daterangepicker table { + width: 100%; + margin: 0; +} + +.daterangepicker td, .daterangepicker th { + text-align: center; + width: 20px; + height: 20px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + cursor: pointer; + white-space: nowrap; +} + +.daterangepicker td.off { + color: #999; +} + +.daterangepicker td.disabled { + color: #999; +} + +.daterangepicker td.available:hover, .daterangepicker th.available:hover { + background: #eee; +} + +.daterangepicker td.in-range { + background: #ebf4f8; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +.daterangepicker td.active, .daterangepicker td.active:hover { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.daterangepicker td.week, .daterangepicker th.week { + font-size: 80%; + color: #ccc; +} + +.daterangepicker select.monthselect, .daterangepicker select.yearselect { + font-size: 12px; + padding: 1px; + height: auto; + margin: 0; + cursor: default; +} + +.daterangepicker select.monthselect { + margin-right: 2%; + width: 56%; +} + +.daterangepicker select.yearselect { + width: 40%; +} + +.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.ampmselect { + width: 60px; + margin-bottom: 0; +} diff --git a/Netdisco/share/public/css/netdisco.css b/Netdisco/share/public/css/netdisco.css index 82d42b1f..dbb223d7 100644 --- a/Netdisco/share/public/css/netdisco.css +++ b/Netdisco/share/public/css/netdisco.css @@ -495,6 +495,10 @@ td > form.nd_inline-form { margin-left: 5px !important; width: 152px; } +.nd_sidebar-topinput { + margin-left: 5px !important; + width: 152px; +} /* set the day/mon/year drop-down width */ #nd_days-select { @@ -573,6 +577,10 @@ form .clearfix.success input { padding-top: 3px !important; } +.nd_sidebar-legend { + margin-bottom: 9px; +} + .nd_netmap-sidebar { margin-top: 7px; margin-left: -9px; diff --git a/Netdisco/share/views/ajax/report/ipinventory.tt b/Netdisco/share/views/ajax/report/ipinventory.tt new file mode 100644 index 00000000..d285fc4e --- /dev/null +++ b/Netdisco/share/views/ajax/report/ipinventory.tt @@ -0,0 +1,39 @@ + + + + + + + + + + + [% FOREACH row IN results %] + + [% IF row.time_last && row.node %] + + [% ELSIF row.time_last %] + + [% ELSE %] + + [% END %] + + [% IF row.age %] + [% age = row.age.replace('mon', 'month') %] + [% IF age.match('(day|month|year)') %] + [% age = age.remove('(-)?\d{2}:\d{2}.*$') %] + [% ELSE %] + [% age = age.replace('(\d{2}:\d{2}):\d{2}(\.\d*)$', '$1') %] + [% END %] + [% ELSE %] + [% age = 'Never' %] + [% END %] + + + + [% END %] + +
DeviceDNSLast UsedFirst Discovered
[% row.ip | html_entity %] + [% '  ' IF NOT row.active %] + [% row.ip | html_entity %] + [% row.ip | html_entity %][% row.dns | html_entity %][% age | html_entity %][% row.time_first || 'Never' | html_entity %]
diff --git a/Netdisco/share/views/ajax/report/ipinventory_csv.tt b/Netdisco/share/views/ajax/report/ipinventory_csv.tt new file mode 100644 index 00000000..c67564e4 --- /dev/null +++ b/Netdisco/share/views/ajax/report/ipinventory_csv.tt @@ -0,0 +1,14 @@ +[% USE CSV %] +[% CSV.dump([ 'Device' 'DNS' 'Time Last' 'Time First' ]) %] + +[% FOREACH row IN results %] + [% mylist = [] %] + [% mylist.push(row.ip) %] + [% mylist.push(row.dns) %] + [% mylist.push(row.time_first) %] + [% mylist.push(row.time_last) %] + [% CSV.dump(mylist) %] + +[% END %] + + diff --git a/Netdisco/share/views/js/common.js b/Netdisco/share/views/js/common.js index 986d529e..d49db758 100644 --- a/Netdisco/share/views/js/common.js +++ b/Netdisco/share/views/js/common.js @@ -128,6 +128,8 @@ // on page load, load the content for the active tab [% IF params.tab %] + [% IF params.tab != 'ipinventory' %] $('#[% params.tab %]_form').trigger("submit"); [% END %] + [% END %] }); diff --git a/Netdisco/share/views/js/daterangepicker.js b/Netdisco/share/views/js/daterangepicker.js new file mode 100644 index 00000000..c116b021 --- /dev/null +++ b/Netdisco/share/views/js/daterangepicker.js @@ -0,0 +1,876 @@ +/** +* @version: 1.2 +* @author: Dan Grossman http://www.dangrossman.info/ +* @date: 2013-07-25 +* @copyright: Copyright (c) 2012-2013 Dan Grossman. All rights reserved. +* @license: Licensed under Apache License v2.0. See http://www.apache.org/licenses/LICENSE-2.0 +* @website: http://www.improvely.com/ +*/ +!function ($) { + + var DateRangePicker = function (element, options, cb) { + var hasOptions = typeof options == 'object'; + var localeObject; + + //option defaults + + this.startDate = moment().startOf('day'); + this.endDate = moment().startOf('day'); + this.minDate = false; + this.maxDate = false; + this.dateLimit = false; + + this.showDropdowns = false; + this.showWeekNumbers = false; + this.timePicker = false; + this.timePickerIncrement = 30; + this.timePicker12Hour = true; + this.ranges = {}; + this.opens = 'right'; + + this.buttonClasses = ['btn', 'btn-small']; + this.applyClass = 'btn-success'; + this.cancelClass = 'btn-default'; + + this.format = 'MM/DD/YYYY'; + this.separator = ' - '; + + this.locale = { + applyLabel: 'Apply', + cancelLabel: 'Cancel', + fromLabel: 'From', + toLabel: 'To', + weekLabel: 'W', + customRangeLabel: 'Custom Range', + daysOfWeek: moment()._lang._weekdaysMin.slice(), + monthNames: moment()._lang._monthsShort.slice(), + firstDay: 0 + }; + + this.cb = function () { }; + + // by default, the daterangepicker element is placed at the bottom of HTML body + this.parentEl = 'body'; + + //element that triggered the date range picker + this.element = $(element); + + if (this.element.hasClass('pull-right')) + this.opens = 'left'; + + if (this.element.is('input')) { + this.element.on({ + click: $.proxy(this.show, this), + focus: $.proxy(this.show, this) + }); + } else { + this.element.on('click', $.proxy(this.show, this)); + } + + localeObject = this.locale; + + if (hasOptions) { + if (typeof options.locale == 'object') { + $.each(localeObject, function (property, value) { + localeObject[property] = options.locale[property] || value; + }); + } + + if (options.applyClass) { + this.applyClass = options.applyClass; + } + + if (options.cancelClass) { + this.cancelClass = options.cancelClass; + } + } + + var DRPTemplate = ''; + + this.parentEl = (hasOptions && options.parentEl && $(options.parentEl)) || $(this.parentEl); + //the date range picker + this.container = $(DRPTemplate).appendTo(this.parentEl); + + if (hasOptions) { + + if (typeof options.format == 'string') + this.format = options.format; + + if (typeof options.separator == 'string') + this.separator = options.separator; + + if (typeof options.startDate == 'string') + this.startDate = moment(options.startDate, this.format); + + if (typeof options.endDate == 'string') + this.endDate = moment(options.endDate, this.format); + + if (typeof options.minDate == 'string') + this.minDate = moment(options.minDate, this.format); + + if (typeof options.maxDate == 'string') + this.maxDate = moment(options.maxDate, this.format); + + if (typeof options.startDate == 'object') + this.startDate = moment(options.startDate); + + if (typeof options.endDate == 'object') + this.endDate = moment(options.endDate); + + if (typeof options.minDate == 'object') + this.minDate = moment(options.minDate); + + if (typeof options.maxDate == 'object') + this.maxDate = moment(options.maxDate); + + if (typeof options.ranges == 'object') { + for (var range in options.ranges) { + + var start = moment(options.ranges[range][0]); + var end = moment(options.ranges[range][1]); + + // If we have a min/max date set, bound this range + // to it, but only if it would otherwise fall + // outside of the min/max. + if (this.minDate && start.isBefore(this.minDate)) + start = moment(this.minDate); + + if (this.maxDate && end.isAfter(this.maxDate)) + end = moment(this.maxDate); + + // If the end of the range is before the minimum (if min is set) OR + // the start of the range is after the max (also if set) don't display this + // range option. + if ((this.minDate && end.isBefore(this.minDate)) || (this.maxDate && start.isAfter(this.maxDate))) { + continue; + } + + this.ranges[range] = [start, end]; + } + + var list = '
    '; + for (var range in this.ranges) { + list += '
  • ' + range + '
  • '; + } + list += '
  • ' + this.locale.customRangeLabel + '
  • '; + list += '
'; + this.container.find('.ranges').prepend(list); + } + + if (typeof options.dateLimit == 'object') + this.dateLimit = options.dateLimit; + + // update day names order to firstDay + if (typeof options.locale == 'object') { + + if (typeof options.locale.daysOfWeek == 'object') { + + // Create a copy of daysOfWeek to avoid modification of original + // options object for reusability in multiple daterangepicker instances + this.locale.daysOfWeek = options.locale.daysOfWeek.slice(); + } + + if (typeof options.locale.firstDay == 'number') { + this.locale.firstDay = options.locale.firstDay; + var iterator = options.locale.firstDay; + while (iterator > 0) { + this.locale.daysOfWeek.push(this.locale.daysOfWeek.shift()); + iterator--; + } + } + } + + if (typeof options.opens == 'string') + this.opens = options.opens; + + if (typeof options.showWeekNumbers == 'boolean') { + this.showWeekNumbers = options.showWeekNumbers; + } + + if (typeof options.buttonClasses == 'string') { + this.buttonClasses = [options.buttonClasses]; + } + + if (typeof options.buttonClasses == 'object') { + this.buttonClasses = options.buttonClasses; + } + + if (typeof options.showDropdowns == 'boolean') { + this.showDropdowns = options.showDropdowns; + } + + if (typeof options.timePicker == 'boolean') { + this.timePicker = options.timePicker; + } + + if (typeof options.timePickerIncrement == 'number') { + this.timePickerIncrement = options.timePickerIncrement; + } + + if (typeof options.timePicker12Hour == 'boolean') { + this.timePicker12Hour = options.timePicker12Hour; + } + + } + + if (!this.timePicker) { + this.startDate = this.startDate.startOf('day'); + this.endDate = this.endDate.startOf('day'); + } + + //apply CSS classes to buttons + var c = this.container; + $.each(this.buttonClasses, function (idx, val) { + c.find('button').addClass(val); + }); + + if (this.opens == 'right') { + //swap calendar positions + var left = this.container.find('.calendar.left'); + var right = this.container.find('.calendar.right'); + left.removeClass('left').addClass('right'); + right.removeClass('right').addClass('left'); + } + + if (typeof options == 'undefined' || typeof options.ranges == 'undefined') { + this.container.find('.calendar').show(); + this.move(); + } + + if (typeof cb == 'function') + this.cb = cb; + + this.container.addClass('opens' + this.opens); + + //try parse date if in text input + if (!hasOptions || (typeof options.startDate == 'undefined' && typeof options.endDate == 'undefined')) { + if ($(this.element).is('input[type=text]')) { + var val = $(this.element).val(); + var split = val.split(this.separator); + var start, end; + if (split.length == 2) { + start = moment(split[0], this.format); + end = moment(split[1], this.format); + } + if (start != null && end != null) { + this.startDate = start; + this.endDate = end; + } + } + } + + //state + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + + this.leftCalendar = { + month: moment([this.startDate.year(), this.startDate.month(), 1, this.startDate.hour(), this.startDate.minute()]), + calendar: [] + }; + + this.rightCalendar = { + month: moment([this.endDate.year(), this.endDate.month(), 1, this.endDate.hour(), this.endDate.minute()]), + calendar: [] + }; + + //event listeners + this.container.on('mousedown', $.proxy(this.mousedown, this)); + + this.container.find('.calendar') + .on('click', '.prev', $.proxy(this.clickPrev, this)) + .on('click', '.next', $.proxy(this.clickNext, this)) + .on('click', 'td.available', $.proxy(this.clickDate, this)) + .on('mouseenter', 'td.available', $.proxy(this.enterDate, this)) + .on('mouseleave', 'td.available', $.proxy(this.updateFormInputs, this)) + .on('change', 'select.yearselect', $.proxy(this.updateMonthYear, this)) + .on('change', 'select.monthselect', $.proxy(this.updateMonthYear, this)) + .on('change', 'select.hourselect,select.minuteselect,select.ampmselect', $.proxy(this.updateTime, this)); + + this.container.find('.ranges') + .on('click', 'button.applyBtn', $.proxy(this.clickApply, this)) + .on('click', 'button.cancelBtn', $.proxy(this.clickCancel, this)) + .on('click', '.daterangepicker_start_input,.daterangepicker_end_input', $.proxy(this.showCalendars, this)) + .on('click', 'li', $.proxy(this.clickRange, this)) + .on('mouseenter', 'li', $.proxy(this.enterRange, this)) + .on('mouseleave', 'li', $.proxy(this.updateFormInputs, this)); + + this.element.on('keyup', $.proxy(this.updateFromControl, this)); + + this.updateView(); + this.updateCalendars(); + + }; + + DateRangePicker.prototype = { + + constructor: DateRangePicker, + + mousedown: function (e) { + e.stopPropagation(); + }, + + updateView: function () { + this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()); + this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()); + this.updateFormInputs(); + }, + + updateFormInputs: function () { + this.container.find('input[name=daterangepicker_start]').val(this.startDate.format(this.format)); + this.container.find('input[name=daterangepicker_end]').val(this.endDate.format(this.format)); + + if (this.startDate.isSame(this.endDate) || this.startDate.isBefore(this.endDate)) { + this.container.find('button.applyBtn').removeAttr('disabled'); + } else { + this.container.find('button.applyBtn').attr('disabled', 'disabled'); + } + }, + + updateFromControl: function () { + if (!this.element.is('input')) return; + if (!this.element.val().length) return; + + var dateString = this.element.val().split(this.separator); + var start = moment(dateString[0], this.format); + var end = moment(dateString[1], this.format); + + if (start == null || end == null) return; + if (end.isBefore(start)) return; + + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + + this.startDate = start; + this.endDate = end; + + if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) + this.notify(); + + this.updateCalendars(); + }, + + notify: function () { + this.updateView(); + this.cb(this.startDate, this.endDate); + }, + + move: function () { + var parentOffset = { + top: this.parentEl.offset().top - (this.parentEl.is('body') ? 0 : this.parentEl.scrollTop()), + left: this.parentEl.offset().left - (this.parentEl.is('body') ? 0 : this.parentEl.scrollLeft()) + }; + if (this.opens == 'left') { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, + right: $(window).width() - this.element.offset().left - this.element.outerWidth() - parentOffset.left, + left: 'auto' + }); + if (this.container.offset().left < 0) { + this.container.css({ + right: 'auto', + left: 9 + }); + } + } else { + this.container.css({ + top: this.element.offset().top + this.element.outerHeight() - parentOffset.top, + left: this.element.offset().left - parentOffset.left, + right: 'auto' + }); + if (this.container.offset().left + this.container.outerWidth() > $(window).width()) { + this.container.css({ + left: 'auto', + right: 0 + }); + } + } + }, + + show: function (e) { + this.container.show(); + this.move(); + + if (e) { + e.stopPropagation(); + e.preventDefault(); + } + + $(document).on('mousedown', $.proxy(this.hide, this)); + this.element.trigger('shown', {target: e.target, picker: this}); + }, + + hide: function (e) { + this.container.hide(); + + if (!this.startDate.isSame(this.oldStartDate) || !this.endDate.isSame(this.oldEndDate)) + this.notify(); + + this.oldStartDate = this.startDate.clone(); + this.oldEndDate = this.endDate.clone(); + + $(document).off('mousedown', this.hide); + this.element.trigger('hidden', { picker: this }); + }, + + enterRange: function (e) { + var label = e.target.innerHTML; + if (label == this.locale.customRangeLabel) { + this.updateView(); + } else { + var dates = this.ranges[label]; + this.container.find('input[name=daterangepicker_start]').val(dates[0].format(this.format)); + this.container.find('input[name=daterangepicker_end]').val(dates[1].format(this.format)); + } + }, + + showCalendars: function() { + this.container.find('.calendar').show(); + this.move(); + }, + + updateInputText: function() { + if (this.element.is('input')) + this.element.val(this.startDate.format(this.format) + this.separator + this.endDate.format(this.format)); + }, + + clickRange: function (e) { + var label = e.target.innerHTML; + if (label == this.locale.customRangeLabel) { + this.showCalendars(); + } else { + var dates = this.ranges[label]; + + this.startDate = dates[0]; + this.endDate = dates[1]; + + if (!this.timePicker) { + this.startDate.startOf('day'); + this.endDate.startOf('day'); + } + + this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()).hour(this.startDate.hour()).minute(this.startDate.minute()); + this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()).hour(this.endDate.hour()).minute(this.endDate.minute()); + this.updateCalendars(); + + this.updateInputText(); + + this.container.find('.calendar').hide(); + this.hide(); + } + }, + + clickPrev: function (e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.subtract('month', 1); + } else { + this.rightCalendar.month.subtract('month', 1); + } + this.updateCalendars(); + }, + + clickNext: function (e) { + var cal = $(e.target).parents('.calendar'); + if (cal.hasClass('left')) { + this.leftCalendar.month.add('month', 1); + } else { + this.rightCalendar.month.add('month', 1); + } + this.updateCalendars(); + }, + + enterDate: function (e) { + + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + + if (cal.hasClass('left')) { + this.container.find('input[name=daterangepicker_start]').val(this.leftCalendar.calendar[row][col].format(this.format)); + } else { + this.container.find('input[name=daterangepicker_end]').val(this.rightCalendar.calendar[row][col].format(this.format)); + } + + }, + + clickDate: function (e) { + var title = $(e.target).attr('data-title'); + var row = title.substr(1, 1); + var col = title.substr(3, 1); + var cal = $(e.target).parents('.calendar'); + + if (cal.hasClass('left')) { + var startDate = this.leftCalendar.calendar[row][col]; + var endDate = this.endDate; + if (typeof this.dateLimit == 'object') { + var maxDate = moment(startDate).add(this.dateLimit).startOf('day'); + if (endDate.isAfter(maxDate)) { + endDate = maxDate; + } + } + } else { + var startDate = this.startDate; + var endDate = this.rightCalendar.calendar[row][col]; + if (typeof this.dateLimit == 'object') { + var minDate = moment(endDate).subtract(this.dateLimit).startOf('day'); + if (startDate.isBefore(minDate)) { + startDate = minDate; + } + } + } + + cal.find('td').removeClass('active'); + + if (startDate.isSame(endDate) || startDate.isBefore(endDate)) { + $(e.target).addClass('active'); + this.startDate = startDate; + this.endDate = endDate; + } else if (startDate.isAfter(endDate)) { + $(e.target).addClass('active'); + this.startDate = startDate; + this.endDate = moment(startDate).add('day', 1).startOf('day'); + } + + this.leftCalendar.month.month(this.startDate.month()).year(this.startDate.year()); + this.rightCalendar.month.month(this.endDate.month()).year(this.endDate.year()); + this.updateCalendars(); + }, + + clickApply: function (e) { + this.updateInputText(); + this.hide(); + }, + + clickCancel: function (e) { + this.startDate = this.oldStartDate; + this.endDate = this.oldEndDate; + this.updateView(); + this.updateCalendars(); + this.hide(); + }, + + updateMonthYear: function (e) { + + var isLeft = $(e.target).closest('.calendar').hasClass('left'); + var cal = this.container.find('.calendar.left'); + if (!isLeft) + cal = this.container.find('.calendar.right'); + + // Month must be Number for new moment versions + var month = parseInt(cal.find('.monthselect').val(), 10); + var year = cal.find('.yearselect').val(); + + if (isLeft) { + this.leftCalendar.month.month(month).year(year); + } else { + this.rightCalendar.month.month(month).year(year); + } + + this.updateCalendars(); + + }, + + updateTime: function(e) { + + var isLeft = $(e.target).closest('.calendar').hasClass('left'); + var cal = this.container.find('.calendar.left'); + if (!isLeft) + cal = this.container.find('.calendar.right'); + + var hour = parseInt(cal.find('.hourselect').val()); + var minute = parseInt(cal.find('.minuteselect').val()); + + if (this.timePicker12Hour) { + var ampm = cal.find('.ampmselect').val(); + if (ampm == 'PM' && hour < 12) + hour += 12; + if (ampm == 'AM' && hour == 12) + hour = 0; + } + + if (isLeft) { + var start = this.startDate.clone(); + start.hour(hour); + start.minute(minute); + this.startDate = start; + this.leftCalendar.month.hour(hour).minute(minute); + } else { + var end = this.endDate.clone(); + end.hour(hour); + end.minute(minute); + this.endDate = end; + this.rightCalendar.month.hour(hour).minute(minute); + } + + this.updateCalendars(); + + }, + + updateCalendars: function () { + this.leftCalendar.calendar = this.buildCalendar(this.leftCalendar.month.month(), this.leftCalendar.month.year(), this.leftCalendar.month.hour(), this.leftCalendar.month.minute(), 'left'); + this.rightCalendar.calendar = this.buildCalendar(this.rightCalendar.month.month(), this.rightCalendar.month.year(), this.rightCalendar.month.hour(), this.rightCalendar.month.minute(), 'right'); + this.container.find('.calendar.left').html(this.renderCalendar(this.leftCalendar.calendar, this.startDate, this.minDate, this.maxDate)); + this.container.find('.calendar.right').html(this.renderCalendar(this.rightCalendar.calendar, this.endDate, this.startDate, this.maxDate)); + + this.container.find('.ranges li').removeClass('active'); + var customRange = true; + var i = 0; + for (var range in this.ranges) { + if (this.timePicker) { + if (this.startDate.isSame(this.ranges[range][0]) && this.endDate.isSame(this.ranges[range][1])) { + customRange = false; + this.container.find('.ranges li:eq(' + i + ')').addClass('active'); + } + } else { + //ignore times when comparing dates if time picker is not enabled + if (this.startDate.format('YYYY-MM-DD') == this.ranges[range][0].format('YYYY-MM-DD') && this.endDate.format('YYYY-MM-DD') == this.ranges[range][1].format('YYYY-MM-DD')) { + customRange = false; + this.container.find('.ranges li:eq(' + i + ')').addClass('active'); + } + } + i++; + } + if (customRange) + this.container.find('.ranges li:last').addClass('active'); + }, + + buildCalendar: function (month, year, hour, minute, side) { + + var firstDay = moment([year, month, 1]); + var lastMonth = moment(firstDay).subtract('month', 1).month(); + var lastYear = moment(firstDay).subtract('month', 1).year(); + + var daysInLastMonth = moment([lastYear, lastMonth]).daysInMonth(); + + var dayOfWeek = firstDay.day(); + + //initialize a 6 rows x 7 columns array for the calendar + var calendar = []; + for (var i = 0; i < 6; i++) { + calendar[i] = []; + } + + //populate the calendar with date objects + var startDay = daysInLastMonth - dayOfWeek + this.locale.firstDay + 1; + if (startDay > daysInLastMonth) + startDay -= 7; + + if (dayOfWeek == this.locale.firstDay) + startDay = daysInLastMonth - 6; + + var curDate = moment([lastYear, lastMonth, startDay, 12, minute]); + for (var i = 0, col = 0, row = 0; i < 42; i++, col++, curDate = moment(curDate).add('hour', 24)) { + if (i > 0 && col % 7 == 0) { + col = 0; + row++; + } + calendar[row][col] = curDate.clone().hour(hour); + curDate.hour(12); + } + + return calendar; + + }, + + renderDropdowns: function (selected, minDate, maxDate) { + var currentMonth = selected.month(); + var monthHtml = '"; + + var currentYear = selected.year(); + var maxYear = (maxDate && maxDate.year()) || (currentYear + 5); + var minYear = (minDate && minDate.year()) || (currentYear - 50); + var yearHtml = ''; + + return monthHtml + yearHtml; + }, + + renderCalendar: function (calendar, selected, minDate, maxDate) { + + var html = '
'; + html += ''; + html += ''; + html += ''; + + // add empty cell for week number + if (this.showWeekNumbers) + html += ''; + + if (!minDate || minDate.isBefore(calendar[1][1])) { + html += ''; + } else { + html += ''; + } + + var dateHtml = this.locale.monthNames[calendar[1][1].month()] + calendar[1][1].format(" YYYY"); + + if (this.showDropdowns) { + dateHtml = this.renderDropdowns(calendar[1][1], minDate, maxDate); + } + + html += ''; + if (!maxDate || maxDate.isAfter(calendar[1][1])) { + html += ''; + } else { + html += ''; + } + + html += ''; + html += ''; + + // add week number label + if (this.showWeekNumbers) + html += ''; + + $.each(this.locale.daysOfWeek, function (index, dayOfWeek) { + html += ''; + }); + + html += ''; + html += ''; + html += ''; + + for (var row = 0; row < 6; row++) { + html += ''; + + // add week number + if (this.showWeekNumbers) + html += ''; + + for (var col = 0; col < 7; col++) { + var cname = 'available '; + cname += (calendar[row][col].month() == calendar[1][1].month()) ? '' : 'off'; + + if ((minDate && calendar[row][col].isBefore(minDate)) || (maxDate && calendar[row][col].isAfter(maxDate))) { + cname = ' off disabled '; + } else if (calendar[row][col].format('YYYY-MM-DD') == selected.format('YYYY-MM-DD')) { + cname += ' active '; + if (calendar[row][col].format('YYYY-MM-DD') == this.startDate.format('YYYY-MM-DD')) { + cname += ' start-date '; + } + if (calendar[row][col].format('YYYY-MM-DD') == this.endDate.format('YYYY-MM-DD')) { + cname += ' end-date '; + } + } else if (calendar[row][col] >= this.startDate && calendar[row][col] <= this.endDate) { + cname += ' in-range '; + if (calendar[row][col].isSame(this.startDate)) { cname += ' start-date '; } + if (calendar[row][col].isSame(this.endDate)) { cname += ' end-date '; } + } + + var title = 'r' + row + 'c' + col; + html += ''; + } + html += ''; + } + + html += ''; + html += '
' + dateHtml + '
' + this.locale.weekLabel + '' + dayOfWeek + '
' + calendar[row][0].week() + '' + calendar[row][col].date() + '
'; + html += '
'; + + if (this.timePicker) { + + html += '
'; + html += ' : '; + + html += ' '; + + if (this.timePicker12Hour) { + html += ''; + } + + html += '
'; + + } + + return html; + + } + + }; + + $.fn.daterangepicker = function (options, cb) { + this.each(function () { + var el = $(this); + if (!el.data('daterangepicker')) + el.data('daterangepicker', new DateRangePicker(el, options, cb)); + }); + return this; + }; + +}(window.jQuery); diff --git a/Netdisco/share/views/js/moment.min.js b/Netdisco/share/views/js/moment.min.js new file mode 100644 index 00000000..45d9c14f --- /dev/null +++ b/Netdisco/share/views/js/moment.min.js @@ -0,0 +1,6 @@ +// moment.js +// version : 2.1.0 +// author : Tim Wood +// license : MIT +// momentjs.com +!function(t){function e(t,e){return function(n){return u(t.call(this,n),e)}}function n(t,e){return function(n){return this.lang().ordinal(t.call(this,n),e)}}function s(){}function i(t){a(this,t)}function r(t){var e=t.years||t.year||t.y||0,n=t.months||t.month||t.M||0,s=t.weeks||t.week||t.w||0,i=t.days||t.day||t.d||0,r=t.hours||t.hour||t.h||0,a=t.minutes||t.minute||t.m||0,o=t.seconds||t.second||t.s||0,u=t.milliseconds||t.millisecond||t.ms||0;this._input=t,this._milliseconds=u+1e3*o+6e4*a+36e5*r,this._days=i+7*s,this._months=n+12*e,this._data={},this._bubble()}function a(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}function o(t){return 0>t?Math.ceil(t):Math.floor(t)}function u(t,e){for(var n=t+"";n.lengthn;n++)~~t[n]!==~~e[n]&&r++;return r+i}function f(t){return t?ie[t]||t.toLowerCase().replace(/(.)s$/,"$1"):t}function l(t,e){return e.abbr=t,x[t]||(x[t]=new s),x[t].set(e),x[t]}function _(t){if(!t)return H.fn._lang;if(!x[t]&&A)try{require("./lang/"+t)}catch(e){return H.fn._lang}return x[t]}function m(t){return t.match(/\[.*\]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"")}function y(t){var e,n,s=t.match(E);for(e=0,n=s.length;n>e;e++)s[e]=ue[s[e]]?ue[s[e]]:m(s[e]);return function(i){var r="";for(e=0;n>e;e++)r+=s[e]instanceof Function?s[e].call(i,t):s[e];return r}}function M(t,e){function n(e){return t.lang().longDateFormat(e)||e}for(var s=5;s--&&N.test(e);)e=e.replace(N,n);return re[e]||(re[e]=y(e)),re[e](t)}function g(t,e){switch(t){case"DDDD":return V;case"YYYY":return X;case"YYYYY":return $;case"S":case"SS":case"SSS":case"DDD":return I;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return R;case"a":case"A":return _(e._l)._meridiemParse;case"X":return B;case"Z":case"ZZ":return j;case"T":return q;case"MM":case"DD":case"YY":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":return J;default:return new RegExp(t.replace("\\",""))}}function p(t){var e=(j.exec(t)||[])[0],n=(e+"").match(ee)||["-",0,0],s=+(60*n[1])+~~n[2];return"+"===n[0]?-s:s}function D(t,e,n){var s,i=n._a;switch(t){case"M":case"MM":i[1]=null==e?0:~~e-1;break;case"MMM":case"MMMM":s=_(n._l).monthsParse(e),null!=s?i[1]=s:n._isValid=!1;break;case"D":case"DD":case"DDD":case"DDDD":null!=e&&(i[2]=~~e);break;case"YY":i[0]=~~e+(~~e>68?1900:2e3);break;case"YYYY":case"YYYYY":i[0]=~~e;break;case"a":case"A":n._isPm=_(n._l).isPM(e);break;case"H":case"HH":case"h":case"hh":i[3]=~~e;break;case"m":case"mm":i[4]=~~e;break;case"s":case"ss":i[5]=~~e;break;case"S":case"SS":case"SSS":i[6]=~~(1e3*("0."+e));break;case"X":n._d=new Date(1e3*parseFloat(e));break;case"Z":case"ZZ":n._useUTC=!0,n._tzm=p(e)}null==e&&(n._isValid=!1)}function Y(t){var e,n,s=[];if(!t._d){for(e=0;7>e;e++)t._a[e]=s[e]=null==t._a[e]?2===e?1:0:t._a[e];s[3]+=~~((t._tzm||0)/60),s[4]+=~~((t._tzm||0)%60),n=new Date(0),t._useUTC?(n.setUTCFullYear(s[0],s[1],s[2]),n.setUTCHours(s[3],s[4],s[5],s[6])):(n.setFullYear(s[0],s[1],s[2]),n.setHours(s[3],s[4],s[5],s[6])),t._d=n}}function w(t){var e,n,s=t._f.match(E),i=t._i;for(t._a=[],e=0;eo&&(u=o,s=n);a(t,s)}function v(t){var e,n=t._i,s=K.exec(n);if(s){for(t._f="YYYY-MM-DD"+(s[2]||" "),e=0;4>e;e++)if(te[e][1].exec(n)){t._f+=te[e][0];break}j.exec(n)&&(t._f+=" Z"),w(t)}else t._d=new Date(n)}function T(e){var n=e._i,s=G.exec(n);n===t?e._d=new Date:s?e._d=new Date(+s[1]):"string"==typeof n?v(e):d(n)?(e._a=n.slice(0),Y(e)):e._d=n instanceof Date?new Date(+n):new Date(n)}function b(t,e,n,s,i){return i.relativeTime(e||1,!!n,t,s)}function S(t,e,n){var s=W(Math.abs(t)/1e3),i=W(s/60),r=W(i/60),a=W(r/24),o=W(a/365),u=45>s&&["s",s]||1===i&&["m"]||45>i&&["mm",i]||1===r&&["h"]||22>r&&["hh",r]||1===a&&["d"]||25>=a&&["dd",a]||45>=a&&["M"]||345>a&&["MM",W(a/30)]||1===o&&["y"]||["yy",o];return u[2]=e,u[3]=t>0,u[4]=n,b.apply({},u)}function F(t,e,n){var s,i=n-e,r=n-t.day();return r>i&&(r-=7),i-7>r&&(r+=7),s=H(t).add("d",r),{week:Math.ceil(s.dayOfYear()/7),year:s.year()}}function O(t){var e=t._i,n=t._f;return null===e||""===e?null:("string"==typeof e&&(t._i=e=_().preparse(e)),H.isMoment(e)?(t=a({},e),t._d=new Date(+e._d)):n?d(n)?k(t):w(t):T(t),new i(t))}function z(t,e){H.fn[t]=H.fn[t+"s"]=function(t){var n=this._isUTC?"UTC":"";return null!=t?(this._d["set"+n+e](t),H.updateOffset(this),this):this._d["get"+n+e]()}}function C(t){H.duration.fn[t]=function(){return this._data[t]}}function L(t,e){H.duration.fn["as"+t]=function(){return+this/e}}for(var H,P,U="2.1.0",W=Math.round,x={},A="undefined"!=typeof module&&module.exports,G=/^\/?Date\((\-?\d+)/i,Z=/(\-)?(\d*)?\.?(\d+)\:(\d+)\:(\d+)\.?(\d{3})?/,E=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g,N=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,J=/\d\d?/,I=/\d{1,3}/,V=/\d{3}/,X=/\d{1,4}/,$=/[+\-]?\d{1,6}/,R=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,j=/Z|[\+\-]\d\d:?\d\d/i,q=/T/i,B=/[\+\-]?\d+(\.\d{1,3})?/,K=/^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/,Q="YYYY-MM-DDTHH:mm:ssZ",te=[["HH:mm:ss.S",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ee=/([\+\-]|\d\d)/gi,ne="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),se={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},ie={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",w:"week",M:"month",y:"year"},re={},ae="DDD w W M D d".split(" "),oe="M D H h m s w W".split(" "),ue={M:function(){return this.month()+1},MMM:function(t){return this.lang().monthsShort(this,t)},MMMM:function(t){return this.lang().months(this,t)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(t){return this.lang().weekdaysMin(this,t)},ddd:function(t){return this.lang().weekdaysShort(this,t)},dddd:function(t){return this.lang().weekdays(this,t)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return u(this.year()%100,2)},YYYY:function(){return u(this.year(),4)},YYYYY:function(){return u(this.year(),5)},gg:function(){return u(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return u(this.weekYear(),5)},GG:function(){return u(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return u(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return~~(this.milliseconds()/100)},SS:function(){return u(~~(this.milliseconds()/10),2)},SSS:function(){return u(this.milliseconds(),3)},Z:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(t/60),2)+":"+u(~~t%60,2)},ZZ:function(){var t=-this.zone(),e="+";return 0>t&&(t=-t,e="-"),e+u(~~(10*t/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}};ae.length;)P=ae.pop(),ue[P+"o"]=n(ue[P],P);for(;oe.length;)P=oe.pop(),ue[P+P]=e(ue[P],2);for(ue.DDDD=e(ue.DDD,3),s.prototype={set:function(t){var e,n;for(n in t)e=t[n],"function"==typeof e?this[n]=e:this["_"+n]=e},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(t){return this._months[t.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(t){return this._monthsShort[t.month()]},monthsParse:function(t){var e,n,s;for(this._monthsParse||(this._monthsParse=[]),e=0;12>e;e++)if(this._monthsParse[e]||(n=H([2e3,e]),s="^"+this.months(n,"")+"|^"+this.monthsShort(n,""),this._monthsParse[e]=new RegExp(s.replace(".",""),"i")),this._monthsParse[e].test(t))return e},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(t){return this._weekdays[t.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(t){return this._weekdaysShort[t.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(t){return this._weekdaysMin[t.day()]},weekdaysParse:function(t){var e,n,s;for(this._weekdaysParse||(this._weekdaysParse=[]),e=0;7>e;e++)if(this._weekdaysParse[e]||(n=H([2e3,1]).day(e),s="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[e]=new RegExp(s.replace(".",""),"i")),this._weekdaysParse[e].test(t))return e},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(t){var e=this._longDateFormat[t];return!e&&this._longDateFormat[t.toUpperCase()]&&(e=this._longDateFormat[t.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t]=e),e},isPM:function(t){return"p"===(t+"").toLowerCase()[0]},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(t,e,n){return t>11?n?"pm":"PM":n?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(t,e){var n=this._calendar[t];return"function"==typeof n?n.apply(e):n},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(t,e,n,s){var i=this._relativeTime[n];return"function"==typeof i?i(t,e,n,s):i.replace(/%d/i,t)},pastFuture:function(t,e){var n=this._relativeTime[t>0?"future":"past"];return"function"==typeof n?n(e):n.replace(/%s/i,e)},ordinal:function(t){return this._ordinal.replace("%d",t)},_ordinal:"%d",preparse:function(t){return t},postformat:function(t){return t},week:function(t){return F(t,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6}},H=function(t,e,n){return O({_i:t,_f:e,_l:n,_isUTC:!1})},H.utc=function(t,e,n){return O({_useUTC:!0,_isUTC:!0,_l:n,_i:t,_f:e})},H.unix=function(t){return H(1e3*t)},H.duration=function(t,e){var n,s,i=H.isDuration(t),a="number"==typeof t,o=i?t._input:a?{}:t,u=Z.exec(t);return a?e?o[e]=t:o.milliseconds=t:u&&(n="-"===u[1]?-1:1,o={y:0,d:~~u[2]*n,h:~~u[3]*n,m:~~u[4]*n,s:~~u[5]*n,ms:~~u[6]*n}),s=new r(o),i&&t.hasOwnProperty("_lang")&&(s._lang=t._lang),s},H.version=U,H.defaultFormat=Q,H.updateOffset=function(){},H.lang=function(t,e){return t?(e?l(t,e):x[t]||_(t),H.duration.fn._lang=H.fn._lang=_(t),void 0):H.fn._lang._abbr},H.langData=function(t){return t&&t._lang&&t._lang._abbr&&(t=t._lang._abbr),_(t)},H.isMoment=function(t){return t instanceof i},H.isDuration=function(t){return t instanceof r},H.fn=i.prototype={clone:function(){return H(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return M(H(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var t=this;return[t.year(),t.month(),t.date(),t.hours(),t.minutes(),t.seconds(),t.milliseconds()]},isValid:function(){return null==this._isValid&&(this._isValid=this._a?!c(this._a,(this._isUTC?H.utc(this._a):H(this._a)).toArray()):!isNaN(this._d.getTime())),!!this._isValid},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(t){var e=M(this,t||H.defaultFormat);return this.lang().postformat(e)},add:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,1),this},subtract:function(t,e){var n;return n="string"==typeof t?H.duration(+e,t):H.duration(t,e),h(this,n,-1),this},diff:function(t,e,n){var s,i,r=this._isUTC?H(t).zone(this._offset||0):H(t).local(),a=6e4*(this.zone()-r.zone());return e=f(e),"year"===e||"month"===e?(s=432e5*(this.daysInMonth()+r.daysInMonth()),i=12*(this.year()-r.year())+(this.month()-r.month()),i+=(this-H(this).startOf("month")-(r-H(r).startOf("month")))/s,i-=6e4*(this.zone()-H(this).startOf("month").zone()-(r.zone()-H(r).startOf("month").zone()))/s,"year"===e&&(i/=12)):(s=this-r,i="second"===e?s/1e3:"minute"===e?s/6e4:"hour"===e?s/36e5:"day"===e?(s-a)/864e5:"week"===e?(s-a)/6048e5:s),n?i:o(i)},from:function(t,e){return H.duration(this.diff(t)).lang(this.lang()._abbr).humanize(!e)},fromNow:function(t){return this.from(H(),t)},calendar:function(){var t=this.diff(H().startOf("day"),"days",!0),e=-6>t?"sameElse":-1>t?"lastWeek":0>t?"lastDay":1>t?"sameDay":2>t?"nextDay":7>t?"nextWeek":"sameElse";return this.format(this.lang().calendar(e,this))},isLeapYear:function(){var t=this.year();return 0===t%4&&0!==t%100||0===t%400},isDST:function(){return this.zone()+H(t).startOf(e)},isBefore:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)<+H(t).startOf(e)},isSame:function(t,e){return e="undefined"!=typeof e?e:"millisecond",+this.clone().startOf(e)===+H(t).startOf(e)},min:function(t){return t=H.apply(null,arguments),this>t?this:t},max:function(t){return t=H.apply(null,arguments),t>this?this:t},zone:function(t){var e=this._offset||0;return null==t?this._isUTC?e:this._d.getTimezoneOffset():("string"==typeof t&&(t=p(t)),Math.abs(t)<16&&(t=60*t),this._offset=t,this._isUTC=!0,e!==t&&h(this,H.duration(e-t,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},daysInMonth:function(){return H.utc([this.year(),this.month()+1,0]).date()},dayOfYear:function(t){var e=W((H(this).startOf("day")-H(this).startOf("year"))/864e5)+1;return null==t?e:this.add("d",t-e)},weekYear:function(t){var e=F(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==t?e:this.add("y",t-e)},isoWeekYear:function(t){var e=F(this,1,4).year;return null==t?e:this.add("y",t-e)},week:function(t){var e=this.lang().week(this);return null==t?e:this.add("d",7*(t-e))},isoWeek:function(t){var e=F(this,1,4).week;return null==t?e:this.add("d",7*(t-e))},weekday:function(t){var e=(this._d.getDay()+7-this.lang()._week.dow)%7;return null==t?e:this.add("d",t-e)},isoWeekday:function(t){return null==t?this.day()||7:this.day(this.day()%7?t:t-7)},lang:function(e){return e===t?this._lang:(this._lang=_(e),this)}},P=0;P + [% FOREACH add_css IN settings._additional_css %] diff --git a/Netdisco/share/views/sidebar/report/ipinventory.tt b/Netdisco/share/views/sidebar/report/ipinventory.tt new file mode 100644 index 00000000..69ccbea8 --- /dev/null +++ b/Netdisco/share/views/sidebar/report/ipinventory.tt @@ -0,0 +1,98 @@ + +
+ +
+ +
+ + + +
+
    +
  • +
    + + +
    +
  • +
+ Seen within the date range:
+ +
+ +
+ + + +
+
    +
  • + Limit:
    + +
  • +
  • + Order By:
    + +
  • +
+
+ + +
+
+
+ + + +