[#75] Device module inventory report / search

This commit is contained in:
Eric A. Miller
2014-02-15 22:07:55 -05:00
parent 8cee58c88e
commit ad8bcc3dfc
9 changed files with 419 additions and 2 deletions

View File

@@ -1,5 +1,9 @@
2.023002 -
[NEW FEATURES]
* [#75] Device module inventory report / search
[ENHANCEMENTS]
* Kwalitee fixes

View File

@@ -0,0 +1,107 @@
package App::Netdisco::DB::ResultSet::DeviceModule;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings FATAL => 'all';
=head1 ADDITIONAL METHODS
=head2 search_by_field( \%cond, \%attrs? )
This variant of the standard C<search()> method returns a ResultSet of Device
Module entries. It is written to support web forms which accept fields that
match and locate Device Modules in the database.
The hashref parameter should contain fields from the Device Module table
which will be intelligently used in a search query.
In addition, you can provide the key C<matchall> which, given a True or False
value, controls whether fields must all match or whether any can match, to
select a row.
Supported keys:
=over 4
=item matchall
If a True value, fields must all match to return a given row of the Device
table, otherwise any field matching will cause the row to be included in
results.
=item description
Can match the C<description> field as a substring.
=item name
Can match the C<name> field as a substring.
=item type
Can match the C<type> field as a substring.
=item model
Can match the C<model> field as a substring.
=item serial
Can match the C<serial> field as a substring.
=item class
Will match exactly the C<class> field.
=item ips
List of Device IPs containing modules.
=back
=cut
sub search_by_field {
my ( $rs, $p, $attrs ) = @_;
die "condition parameter to search_by_field must be hashref\n"
if ref {} ne ref $p
or 0 == scalar keys %$p;
my $op = $p->{matchall} ? '-and' : '-or';
return $rs->search_rs( {}, $attrs )->search(
{ $op => [
( $p->{description}
? ( 'me.description' =>
{ '-ilike' => "\%$p->{description}\%" } )
: ()
),
( $p->{name}
? ( 'me.name' => { '-ilike' => "\%$p->{name}\%" } )
: ()
),
( $p->{type}
? ( 'me.type' => { '-ilike' => "\%$p->{type}\%" } )
: ()
),
( $p->{model}
? ( 'me.model' => { '-ilike' => "\%$p->{model}\%" } )
: ()
),
( $p->{serial}
? ( 'me.serial' => { '-ilike' => "\%$p->{serial}\%" } )
: ()
),
( $p->{class}
? ( 'me.class' => { '-in' => $p->{class} } )
: ()
),
( $p->{ips} ? ( 'me.ip' => { '-in' => $p->{ips} } ) : () ),
],
}
);
}
1;

View File

@@ -0,0 +1,109 @@
package App::Netdisco::Web::Plugin::Report::ModuleInventory;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use List::MoreUtils ();
register_report(
{ category => 'Device',
tag => 'moduleinventory',
label => 'Module Inventory',
provides_csv => 1,
}
);
hook 'before' => sub {
# view settings
var('module_options' => [
{ name => 'matchall',
label => 'Match All Options',
default => 'on'
},
]
);
return
unless (
request->path eq uri_for('/report/moduleinventory')->path
or index( request->path,
uri_for('/ajax/content/report/moduleinventory')->path ) == 0
);
params->{'limit'} ||= 1024;
foreach my $col ( @{ var('module_options') } ) {
next unless $col->{default} eq 'on';
params->{ $col->{name} } = 'checked';
}
};
hook 'before_template' => sub {
my $tokens = shift;
return
unless (
request->path eq uri_for('/report/moduleinventory')->path
or index( request->path,
uri_for('/ajax/content/report/moduleinventory')->path ) == 0
);
# used in the search sidebar template to set selected items
foreach my $opt (qw/class/) {
my $p = (
ref [] eq ref param($opt)
? param($opt)
: ( param($opt) ? [ param($opt) ] : [] )
);
$tokens->{"${opt}_lkp"} = { map { $_ => 1 } @$p };
}
};
get '/ajax/content/report/moduleinventory' => require_login sub {
my $has_opt = List::MoreUtils::any { param($_) }
qw/device description name type model serial class/;
my $rs = schema('netdisco')->resultset('DeviceModule');
if ($has_opt) {
if ( param('device') ) {
my @ips = schema('netdisco')->resultset('Device')
->search_fuzzy( param('device') )->get_column('ip')->all;
params->{'ips'} = \@ips;
}
$rs = $rs->search_by_field( scalar params )->prefetch('device')
->limit( param('limit') )->hri;
}
else {
$rs = $rs->search(
{},
{ select => [ 'class', { count => 'class' } ],
as => [qw/ class count /],
group_by => [qw/ class /]
}
)->order_by( { -desc => 'count' } )->hri;
}
return unless $rs->has_rows;
if ( request->is_ajax ) {
template 'ajax/report/moduleinventory.tt', { results => $rs, },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/moduleinventory_csv.tt', { results => $rs, },
{ layout => undef };
}
};
1;

View File

@@ -12,6 +12,8 @@ get '/report/*' => require_login sub {
= [
schema('netdisco')->resultset('NodeNbt')->get_distinct_col('domain')
];
my $class_list = [ schema('netdisco')->resultset('DeviceModule')
->get_distinct_col('class') ];
# trick the ajax into working as if this were a tabbed page
params->{tab} = $tag;
@@ -21,6 +23,7 @@ get '/report/*' => require_login sub {
{
report => setting('_reports')->{$tag},
domain_list => $domain_list,
class_list => $class_list,
};
};

View File

@@ -47,6 +47,7 @@ web_plugins:
- Report::DevicePoeStatus
- Report::DuplexMismatch
- Report::IpInventory
- Report::ModuleInventory
- Report::Netbios
- Report::NodeMultiIPs
- Report::PhonesDiscovered

View File

@@ -0,0 +1,86 @@
[% USE Number.Format %]
[% IF results.first.ip %]
[% row = results.reset %]
<table class="table table-bordered table-condensed table-striped nd_floatinghead">
<thead>
<tr>
<th>Device</th>
<th>Description</th>
<th>Name</th>
<th>Class</th>
<th>Type</th>
<th>Model</th>
<th>Serial</th>
<th>HW Version</th>
<th>SW Version</th>
<th>FW Version</th>
</tr>
</thead>
</tbody>
[% WHILE (row = results.next) %]
<tr>
<td>
<a class="nd_linkcell"
href="[% uri_for('/device') %]?tab=modules&q=[% row.device.ip | uri %]">
[% row.device.dns || row.device.name || row.device.ip | html_entity %]</a>
</td>
<td>
<a class="nd_linkcell"
href="[% uri_for('/report/moduleinventory') %]?description=[% row.description | uri %]">
[% row.description | html %]</a>
</td>
<td>
<a class="nd_linkcell"
href="[% uri_for('/report/moduleinventory') %]?name=[% row.name | uri %]">
[% row.name | html %]</a>
</td>
<td>
<a class="nd_linkcell"
href="[% uri_for('/report/moduleinventory') %]?class=[% row.class | uri %]">
[% row.class.ucfirst | html %]</a>
</td>
<td>
<a class="nd_linkcell"
href="[% uri_for('/report/moduleinventory') %]?type=[% row.type | uri %]">
[% row.type | html %]</a>
</td>
<td>
<a class="nd_linkcell"
href="[% uri_for('/report/moduleinventory') %]?model=[% row.model | uri %]">
[% row.model | html %]</a>
</td>
<td>
<a class="nd_linkcell"
href="[% uri_for('/report/moduleinventory') %]?serial=[% row.serial | uri %]">
[% row.serial | html %]</a>
</td>
<td>[% row.hw_ver | html_entity %]</td>
<td>[% row.sw_ver | html_entity %]</td>
<td>[% row.fw_ver | html_entity %]</td>
</tr>
[% END %]
</tbody>
</table>
[% ELSE %]
[% row = results.reset %]
<table class="table table-bordered table-condensed table-striped nd_floatinghead">
<thead>
<tr>
<th>Class</th>
<th>Count</th>
</tr>
</thead>
</tbody>
[% WHILE (row = results.next) %]
<tr>
<td>
<a class="nd_linkcell"
href="[% uri_for('/report/moduleinventory') %]?class=[% row.class | uri %]">
[% row.class.ucfirst | html %]</a>
</td>
<td>[% row.count | format_number %]</td>
</tr>
[% END %]
</tbody>
</table>
[% END %]

View File

@@ -0,0 +1,27 @@
[% USE CSV -%]
[% IF results.first.ip %]
[% row = results.reset %]
[% CSV.dump(['Device' 'Description' 'Name' 'Class' 'Type' 'Model' 'Serial' 'HW Version' 'SW Version' 'FW Version']) %]
[% WHILE (row = results.next) %]
[% mylist = [] %]
[% device = row.device.dns || row.device.name || row.device.ip %]
[% FOREACH col IN [ device row.description row.name row.class.ucfirst row.type row.model row.serial row.hw_ver row.sw_ver row.fw_ver ] %]
[% mylist.push(col) %]
[% END %]
[% CSV.dump(mylist) %]
[% END %]
[% ELSE %]
[% row = results.reset %]
[% CSV.dump(['Class' 'Count']) %]
[% WHILE (row = results.next) %]
[% mylist = [] %]
[% FOREACH col IN [ row.class.ucfirst row.count ] %]
[% mylist.push(col) %]
[% END %]
[% CSV.dump(mylist) %]
[% END %]
[% END %]

View File

@@ -2,7 +2,7 @@
// ajax content is loaded
var path = 'report';
// fields in the IP Inventory Report form
// colored input fields in the Report Options sidebar forms
var form_inputs = $(".nd_colored-input");
// this is called by do_search to support local code
@@ -15,7 +15,7 @@
$("[rel=popover]").popover({live: true});
}
// on load, check initial Device Search Options form state,
// on load, check initial Report Options form state,
// and on each change to the form fields
$(document).ready(function() {
var tab = '[% report.tag %]'
@@ -25,6 +25,14 @@
form_inputs.each(function() {device_form_state($(this))});
form_inputs.change(function() {device_form_state($(this))});
// handler for bin icon in search forms
$('.nd_field-clear-icon').click(function() {
var name = $(this).data('btn-for');
var input = $('[name=' + name + ']');
input.val('');
device_form_state(input); // reset input field
});
$('#nd_ipinventory-subnet').on('input', function(event) {
if ($(this).val().indexOf(':') != -1) {
$('#never').attr('disabled', 'disabled');

View File

@@ -0,0 +1,72 @@
<p class="nd_sidebar-title"><em>Module Search Options</em></p>
<div class="clearfix">
<i id="device_clear_btn" data-btn-for="device"
class="nd_field-clear-icon icon-trash icon-large"></i>
<input class="nd_side-input nd_colored-input" placeholder="Device"
type="text" name="device" value="[% params.dns | html_entity %]"
rel="tooltip" data-placement="left" data-offset="5" data-title="Device IP, DNS, Description, Name, Location, Serial, or Contact"/>
</div>
<div class="clearfix">
<i id="description_clear_btn" data-btn-for="description"
class="nd_field-clear-icon icon-trash icon-large"></i>
<input class="nd_side-input nd_colored-input" placeholder="Module Description"
type="text" name="description" value="[% params.description | html_entity %]"
rel="tooltip" data-placement="left" data-offset="5" data-title="Module Description"/>
</div>
<div class="clearfix">
<i id="name_clear_btn" data-btn-for="name"
class="nd_field-clear-icon icon-trash icon-large"></i>
<input class="nd_side-input nd_colored-input" placeholder="Module Name"
type="text" name="name" value="[% params.name | html_entity %]"
rel="tooltip" data-placement="left" data-offset="5" data-title="Module Name"/>
</div>
<div class="clearfix">
<i id="type_clear_btn" data-btn-for="type"
class="nd_field-clear-icon icon-trash icon-large"></i>
<input class="nd_side-input nd_colored-input" placeholder="Module Type"
type="text" name="type" value="[% params.type | html_entity %]"
rel="tooltip" data-placement="left" data-offset="5" data-title="Module Type"/>
</div>
<div class="clearfix">
<i id="model_clear_btn" data-btn-for="model"
class="nd_field-clear-icon icon-trash icon-large"></i>
<input class="nd_side-input nd_colored-input" placeholder="Module Model"
type="text" name="model" value="[% params.model | html_entity %]"
rel="tooltip" data-placement="left" data-offset="5" data-title="Module Model"/>
</div>
<div class="clearfix">
<i id="serial_clear_btn" data-btn-for="serial"
class="nd_field-clear-icon icon-trash icon-large"></i>
<input class="nd_side-input nd_colored-input" placeholder="Module Serial"
type="text" name="serial" value="[% params.serial | html_entity %]"
rel="tooltip" data-placement="left" data-offset="5" data-title="Module Serial"/>
</div>
<div class="clearfix">
<select class="nd_side-select nd_colored-input" size="[% class_list.size > 5 ? 5 : class_list.size %]"
multiple="on" name="class"
rel="tooltip" data-placement="left" data-offset="5" data-title="Module Class"/>
[% FOREACH opt IN class_list %]
<option[% ' selected="selected"' IF class_lkp.exists(opt) %]>[% opt | html_entity %]</option>
[% END %]
</select>
</div>
<div class="clearfix">
<em class="muted">Limit:</em><br/>
<select id="nd_mac-format" class="nd_side-select" name="limit">
[% FOREACH size IN [ '128', '256', '1024', '2048', '4096', '8192' ] %]
<option[% ' selected="selected"' IF params.limit == size %]>[% size %]</option>
[% END %]
</select>
</div>
<div class="clearfix input-prepend">
<label class="add-on">
<input type="checkbox" id="matchall"
name="matchall"[% ' checked="checked"' IF params.matchall %]/>
</label>
<label class="nd_checkboxlabel" for="matchall">
<span class="nd_searchcheckbox uneditable-input">Match All Options</span>
</label>
</div>
<button id="[% tab.tag %]_submit" type="submit" class="btn btn-info">
<i class="icon-search icon-large pull-left nd_navbar-icon"></i> Search Modules</button>