diff --git a/lib/App/Netdisco/DB/Result/Admin.pm b/lib/App/Netdisco/DB/Result/Admin.pm index 8e185ad2..71c436f3 100644 --- a/lib/App/Netdisco/DB/Result/Admin.pm +++ b/lib/App/Netdisco/DB/Result/Admin.pm @@ -155,4 +155,12 @@ between the date stamp and time stamp. That is: sub finished_stamp { return (shift)->get_column('finished_stamp') } +=head2 duration + +Difference between started and finished. + +=cut + +sub duration { return (shift)->get_column('duration') } + 1; diff --git a/lib/App/Netdisco/DB/ResultSet/Admin.pm b/lib/App/Netdisco/DB/ResultSet/Admin.pm index 8dc841fd..4be6d77c 100644 --- a/lib/App/Netdisco/DB/ResultSet/Admin.pm +++ b/lib/App/Netdisco/DB/ResultSet/Admin.pm @@ -46,6 +46,8 @@ will add the following additional synthesized columns to the result set: =item finished_stamp +=item duration + =back =cut @@ -58,9 +60,10 @@ sub with_times { ->search({}, { '+columns' => { - entered_stamp => \"to_char(entered, 'YYYY-MM-DD HH24:MI')", - started_stamp => \"to_char(started, 'YYYY-MM-DD HH24:MI')", - finished_stamp => \"to_char(finished, 'YYYY-MM-DD HH24:MI')", + entered_stamp => \"to_char(entered, 'YYYY-MM-DD HH24:MI:SS')", + started_stamp => \"to_char(started, 'YYYY-MM-DD HH24:MI:SS')", + finished_stamp => \"to_char(finished, 'YYYY-MM-DD HH24:MI:SS')", + duration => \"justify_interval(extract(epoch FROM (finished - started)) * interval '1 second')", }, }); } diff --git a/lib/App/Netdisco/JobQueue/PostgreSQL.pm b/lib/App/Netdisco/JobQueue/PostgreSQL.pm index 4d68181c..7f650dd9 100644 --- a/lib/App/Netdisco/JobQueue/PostgreSQL.pm +++ b/lib/App/Netdisco/JobQueue/PostgreSQL.pm @@ -257,10 +257,39 @@ sub jq_complete { sub jq_log { return schema(vars->{'tenant'})->resultset('Admin')->search({ - 'me.action' => { '-not_like' => 'hook::%' }, - -or => [ - { 'me.log' => undef }, - { 'me.log' => { '-not_like' => 'duplicate of %' } }, + (param('backend') ? ( + 'me.status' => { '=' => [ + # FIXME 'done-'. param('backend'), + 'queued-'. param('backend'), + ] }, + ) : ()), + (param('action') ? ('me.action' => param('action')) : ()), + (param('device') ? ( + -or => [ + { 'me.device' => param('device') }, + { 'target.ip' => param('device') }, + ], + ) : ()), + (param('username') ? ('me.username' => param('username')) : ()), + (param('status') ? ('me.status' => lc(param('status'))) : ()), + (param('duration') ? ( + -bool => [ + -or => [ + { + 'me.finished' => undef, + 'me.started' => { '<' => \[q{(CURRENT_TIMESTAMP - ? ::interval)}, param('duration') .' minutes'] }, + }, + -and => [ + { 'me.started' => { '!=' => undef } }, + { 'me.finished' => { '!=' => undef } }, + \[ q{ (me.finished - me.started) > ? ::interval }, param('duration') .' minutes'], + ], + ], + ], + ) : ()), + 'me.log' => [ + { '=' => undef }, + { '-not_like' => 'duplicate of %' }, ], }, { prefetch => 'target', diff --git a/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm b/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm index 5cf97cac..9e2da36b 100644 --- a/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm +++ b/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm @@ -24,6 +24,7 @@ ajax '/ajax/control/admin/jobqueue/delall' => require_role admin => sub { ajax '/ajax/content/admin/jobqueue' => require_role admin => sub { content_type('text/html'); + template 'ajax/admintask/jobqueue.tt', { results => [ jq_log ], }, { layout => undef }; diff --git a/lib/App/Netdisco/Web/TypeAhead.pm b/lib/App/Netdisco/Web/TypeAhead.pm index 29d27616..89b3857e 100644 --- a/lib/App/Netdisco/Web/TypeAhead.pm +++ b/lib/App/Netdisco/Web/TypeAhead.pm @@ -7,6 +7,90 @@ use Dancer::Plugin::Auth::Extensible; use App::Netdisco::Util::Web (); # for sort_port use HTML::Entities 'encode_entities'; +use List::MoreUtils (); + +ajax '/ajax/data/queue/typeahead/backend' => require_role admin => sub { + return '[]' unless setting('navbar_autocomplete'); + + my $q = quotemeta( param('query') || param('term') || param('backend') ); + my @backends = + grep { $q ? m/$q/ : true } + List::MoreUtils::uniq + sort + grep { defined } + schema(vars->{'tenant'})->resultset('DeviceSkip')->get_distinct_col('backend'); + + content_type 'application/json'; + to_json \@backends; +}; + +ajax '/ajax/data/queue/typeahead/username' => require_role admin => sub { + return '[]' unless setting('navbar_autocomplete'); + + my $q = quotemeta( param('query') || param('term') || param('username') ); + my @users = + grep { $q ? m/$q/ : true } + List::MoreUtils::uniq + sort + grep { defined } + schema(vars->{'tenant'})->resultset('Admin')->get_distinct_col('username'); + + content_type 'application/json'; + to_json \@users; +}; + +ajax '/ajax/data/queue/typeahead/action' => require_role admin => sub { + return '[]' unless setting('navbar_autocomplete'); + + my @actions = (); + my @core_plugins = @{ setting('worker_plugins') || [] }; + my @user_plugins = @{ setting('extra_worker_plugins') || [] }; + + # load worker plugins for our action + foreach my $plugin (@user_plugins, @core_plugins) { + $plugin =~ s/^X::/+App::NetdiscoX::Worker::Plugin::/; + $plugin = 'App::Netdisco::Worker::Plugin::'. $plugin + if $plugin !~ m/^\+/; + $plugin =~ s/^\+//; + + next if $plugin =~ m/::Plugin::Internal::/; + + if ($plugin =~ m/::Plugin::(Hook::[^:]+)/) { + push @actions, lc $1; + next; + } + + next unless $plugin =~ m/::Plugin::([^:]+)::/; + push @actions, lc $1; + } + + my $q = quotemeta( param('query') || param('term') || param('action') ); + @actions = + grep { $q ? m/^$q/ : true } + List::MoreUtils::uniq + sort + grep { defined } + @actions, + schema(vars->{'tenant'})->resultset('Admin')->get_distinct_col('action'); + + content_type 'application/json'; + to_json \@actions; +}; + +ajax '/ajax/data/queue/typeahead/status' => require_role admin => sub { + return '[]' unless setting('navbar_autocomplete'); + + my $q = quotemeta( param('query') || param('term') || param('status') ); + my @actions = + grep { $q ? m/^$q/ : true } + List::MoreUtils::uniq + sort + grep { defined } + qw(Queued Done Info Deferred Error); + + content_type 'application/json'; + to_json \@actions; +}; ajax '/ajax/data/devicename/typeahead' => require_login sub { return '[]' unless setting('navbar_autocomplete'); diff --git a/share/public/css/netdisco.css b/share/public/css/netdisco.css index 4a15b9ac..8a712c16 100644 --- a/share/public/css/netdisco.css +++ b/share/public/css/netdisco.css @@ -597,6 +597,17 @@ div.toggle.btn-small { width: 152px; } +/* set the drop-down width */ +.nd_sidebar-narrow-dropdown { + margin-top: 4px; + width: 46px; +} + +/* set the drop-down width */ +.nd_sidebar-dropdown { + width: 130px; +} + /* set the day/mon/year drop-down width */ #nd_days-select { margin-top: 4px; diff --git a/share/views/admintask.tt b/share/views/admintask.tt index 435e7fe6..ea501624 100644 --- a/share/views/admintask.tt +++ b/share/views/admintask.tt @@ -30,11 +30,17 @@ href="#[% task.tag | html_entity %]_pane">[% task.label | html_entity %] [% IF task.tag == 'jobqueue' %] - - - - +    + + + +    + +    + +    + [% ELSIF task.tag == 'userlog' %] diff --git a/share/views/ajax/admintask/jobqueue.tt b/share/views/ajax/admintask/jobqueue.tt index 41864ec0..91a0c1b1 100644 --- a/share/views/ajax/admintask/jobqueue.tt +++ b/share/views/ajax/admintask/jobqueue.tt @@ -4,16 +4,14 @@ - + - - - - - - - + + + + + @@ -24,17 +22,18 @@ [% ' class="nd_jobqueueitem info"' IF row.status.search('^queued-') %] data-content="[% row.log | html_entity %]" > - + [% IF row.status.search('^queued-') %] + + [% ELSE %] + + [% END %] + - [% IF row.status.search('^queued-') %] - - [% ELSE %] - - [% END %] + - - + - - + + [% IF row.status.search('^queued-') %] + + [% ELSE %] + + [% END %] + + + + + + + + + [% END %]
EnteredBackend ActionStatus DevicePortParamUserStartedFinishedActionSubmitted ByStatusDurationDetailsCancel
[% row.entered_stamp | html_entity %][% row.status.remove('^queued-') | html_entity %] [% FOREACH word IN row.action.split('_') %] [% word.ucfirst | html_entity %]  [% END %] Running on "[% row.status.remove('^queued-') | html_entity %]"[% row.status.ucfirst | html_entity %] [% IF row.action == 'discover' AND row.status == 'error' %] [% row.device | html_entity %] @@ -42,16 +41,42 @@ [% row.target.dns || row.device | html_entity %] [% END %] [% row.port | html_entity %][% row.subaction | html_entity %][% row.username | html_entity %][% row.started_stamp | html_entity %][% row.finished_stamp | html_entity %]Running[% row.status.ucfirst | html_entity %][% row.duration | html_entity %] + +
+ + + + + + + + + + +
ID [% row.job | html_entity %]
Entered [% row.entered_stamp | html_entity %]
Started [% row.started_stamp | html_entity %]
Finished [% row.finished_stamp | html_entity %]
Port [% row.port | html_entity %]
Subaction [% row.subaction | html_entity %]
User IP [% row.userip | html_entity %]
Device Key [% row.device_key | html_entity %]
Log [% row.log | html_entity %]
+
diff --git a/share/views/js/admintask.js b/share/views/js/admintask.js index 28573ead..dab656ad 100644 --- a/share/views/js/admintask.js +++ b/share/views/js/admintask.js @@ -13,7 +13,7 @@ function inner_view_processing(tab) { // reload this table every 5 seconds - if (tab == 'jobqueue' + if ((tab == 'jobqueue') && $('#nd_countdown-control-icon').hasClass('icon-play')) { $('#nd_countdown').text(timermax); @@ -40,6 +40,19 @@ }, (timermax * 1000))); } + // activate typeahead on the queue filter boxes + $('.nd_queue_ta').autocomplete({ + source: function (request, response) { + var name = $(this.element)[0].name; + var query = $(this.element).serialize(); + return $.get( uri_base + '/ajax/data/queue/typeahead/' + name, query, function (data) { + return response(data); + }); + } + ,delay: 150 + ,minLength: 0 + }); + // activate typeahead on the topo boxes $('.nd_topo_dev').autocomplete({ source: uri_base + '/ajax/data/deviceip/typeahead' @@ -71,6 +84,15 @@ ,minLength: 0 }); + $('.nd_jobqueue-extra').click(function(event) { + event.preventDefault(); + var icon = $(this).children('i'); + $(icon).toggleClass('icon-plus'); + $(icon).toggleClass('icon-minus'); + var extra_id = $(this).data('extra'); + $('#' + extra_id).toggle(); + }); + // activate modals and tooltips $('.nd_modal').modal({show: false}); $("[rel=tooltip]").tooltip({live: true}); @@ -81,6 +103,18 @@ var tab = '[% task.tag | html_entity %]' var target = '#' + tab + '_pane'; + // get autocomplete field on input focus + $('.nd_sidebar').on('focus', '.nd_queue_ta', function(e) { + $(this).autocomplete('search', '%') }); + $('.nd_sidebar').on('click', '.nd_topo_dev_caret', function(e) { + $(this).siblings('.nd_queue_ta').autocomplete('search', '%') }); + + // get all devices on device input focus + $('.nd_sidebar').on('focus', '.nd_topo_dev', function(e) { + $(this).autocomplete('search', '%') }); + $('.nd_sidebar').on('click', '.nd_topo_dev_caret', function(e) { + $(this).siblings('.nd_topo_dev').autocomplete('search', '%') }); + // get all devices on device input focus $(target).on('focus', '.nd_topo_dev', function(e) { $(this).autocomplete('search', '%') }); @@ -95,12 +129,29 @@ $(this).siblings('.nd_topo_port').autocomplete('search'); }); + // job control sidebar submit should reset timer + // and update bookmark + $('#' + tab + '_submit').click(function(event) { + for (var i = 0; i < nd_timers.length; i++) { + clearTimeout(nd_timers[i]); + } + // reset the timer cache + timercache = timermax - 1; + + // bookmark + var querystr = $('#' + tab + '_form').serialize(); + $('#nd_jobqueue-bookmark').attr('href',uri_base + '/admin/' + tab + '?' + querystr); + }); + // job control refresh icon should reload the page $('#nd_countdown-refresh').click(function(event) { event.preventDefault(); for (var i = 0; i < nd_timers.length; i++) { clearTimeout(nd_timers[i]); } + // reset the timer cache + timercache = timermax - 1; + // and reload content $('#' + tab + '_form').trigger('submit'); }); diff --git a/share/views/sidebar/admintask/jobqueue.tt b/share/views/sidebar/admintask/jobqueue.tt new file mode 100644 index 00000000..07ab9a94 --- /dev/null +++ b/share/views/sidebar/admintask/jobqueue.tt @@ -0,0 +1,49 @@ + + Job Queue Filters + +
+
+
+ + Backend:
+
+ + +
+ + Action:
+
+ + +
+ + Device:
+
+ + +
+ + Submitted by:
+
+ + +
+ + Job status:
+
+ + +
+ + Duration:
+ + +
+ + +