[#75] Device module inventory report / search
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
2.023002 -
|
2.023002 -
|
||||||
|
|
||||||
|
[NEW FEATURES]
|
||||||
|
|
||||||
|
* [#75] Device module inventory report / search
|
||||||
|
|
||||||
[ENHANCEMENTS]
|
[ENHANCEMENTS]
|
||||||
|
|
||||||
* Kwalitee fixes
|
* Kwalitee fixes
|
||||||
|
|||||||
107
Netdisco/lib/App/Netdisco/DB/ResultSet/DeviceModule.pm
Normal file
107
Netdisco/lib/App/Netdisco/DB/ResultSet/DeviceModule.pm
Normal 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;
|
||||||
109
Netdisco/lib/App/Netdisco/Web/Plugin/Report/ModuleInventory.pm
Normal file
109
Netdisco/lib/App/Netdisco/Web/Plugin/Report/ModuleInventory.pm
Normal 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;
|
||||||
@@ -12,6 +12,8 @@ get '/report/*' => require_login sub {
|
|||||||
= [
|
= [
|
||||||
schema('netdisco')->resultset('NodeNbt')->get_distinct_col('domain')
|
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
|
# trick the ajax into working as if this were a tabbed page
|
||||||
params->{tab} = $tag;
|
params->{tab} = $tag;
|
||||||
@@ -21,6 +23,7 @@ get '/report/*' => require_login sub {
|
|||||||
{
|
{
|
||||||
report => setting('_reports')->{$tag},
|
report => setting('_reports')->{$tag},
|
||||||
domain_list => $domain_list,
|
domain_list => $domain_list,
|
||||||
|
class_list => $class_list,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ web_plugins:
|
|||||||
- Report::DevicePoeStatus
|
- Report::DevicePoeStatus
|
||||||
- Report::DuplexMismatch
|
- Report::DuplexMismatch
|
||||||
- Report::IpInventory
|
- Report::IpInventory
|
||||||
|
- Report::ModuleInventory
|
||||||
- Report::Netbios
|
- Report::Netbios
|
||||||
- Report::NodeMultiIPs
|
- Report::NodeMultiIPs
|
||||||
- Report::PhonesDiscovered
|
- Report::PhonesDiscovered
|
||||||
|
|||||||
86
Netdisco/share/views/ajax/report/moduleinventory.tt
Normal file
86
Netdisco/share/views/ajax/report/moduleinventory.tt
Normal 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 %]
|
||||||
27
Netdisco/share/views/ajax/report/moduleinventory_csv.tt
Normal file
27
Netdisco/share/views/ajax/report/moduleinventory_csv.tt
Normal 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 %]
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// ajax content is loaded
|
// ajax content is loaded
|
||||||
var path = 'report';
|
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");
|
var form_inputs = $(".nd_colored-input");
|
||||||
|
|
||||||
// this is called by do_search to support local code
|
// this is called by do_search to support local code
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
$("[rel=popover]").popover({live: true});
|
$("[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
|
// and on each change to the form fields
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
var tab = '[% report.tag %]'
|
var tab = '[% report.tag %]'
|
||||||
@@ -25,6 +25,14 @@
|
|||||||
form_inputs.each(function() {device_form_state($(this))});
|
form_inputs.each(function() {device_form_state($(this))});
|
||||||
form_inputs.change(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) {
|
$('#nd_ipinventory-subnet').on('input', function(event) {
|
||||||
if ($(this).val().indexOf(':') != -1) {
|
if ($(this).val().indexOf(':') != -1) {
|
||||||
$('#never').attr('disabled', 'disabled');
|
$('#never').attr('disabled', 'disabled');
|
||||||
|
|||||||
72
Netdisco/share/views/sidebar/report/moduleinventory.tt
Normal file
72
Netdisco/share/views/sidebar/report/moduleinventory.tt
Normal 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>
|
||||||
Reference in New Issue
Block a user