From 60eeebdba9490f46ccc931863bf8b72ef0653f59 Mon Sep 17 00:00:00 2001 From: James Tolley Date: Tue, 26 Jun 2007 23:48:41 +0000 Subject: [PATCH] spectre/workflows/priorities RFE --- lib/Spectre/Cron.pm | 43 +++-- lib/Spectre/Workflow.pm | 110 ++++++++++- lib/WebGUI/AdminConsole.pm | 9 + lib/WebGUI/Operation.pm | 1 + lib/WebGUI/Operation/Spectre.pm | 64 +++++++ lib/WebGUI/Operation/Workflow.pm | 215 ++++++++++++++++++---- lib/WebGUI/i18n/English/Spectre.pm | 58 ++++++ lib/WebGUI/i18n/English/Workflow.pm | 107 ++++++++++- www/extras/adminConsole/small/spectre.gif | Bin 0 -> 587 bytes www/extras/adminConsole/spectre.gif | Bin 0 -> 2115 bytes 10 files changed, 556 insertions(+), 51 deletions(-) create mode 100644 lib/WebGUI/i18n/English/Spectre.pm create mode 100644 www/extras/adminConsole/small/spectre.gif create mode 100644 www/extras/adminConsole/spectre.gif diff --git a/lib/Spectre/Cron.pm b/lib/Spectre/Cron.pm index d27416dae..8835c9baf 100644 --- a/lib/Spectre/Cron.pm +++ b/lib/Spectre/Cron.pm @@ -19,6 +19,7 @@ use DateTime; use HTTP::Request::Common; use HTTP::Cookies; use POE qw(Component::Client::HTTP); +use JSON 'objToJson'; #------------------------------------------------------------------- @@ -29,12 +30,12 @@ Initializes the scheduler. =cut sub _start { - my ($kernel, $self, $publicEvents) = @_[ KERNEL, OBJECT, ARG0 ]; - $self->debug("Starting Spectre scheduler."); - my $serviceName = "cron"; - $kernel->alias_set($serviceName); - $kernel->call( IKC => publish => $serviceName, $publicEvents ); - $kernel->yield("checkSchedules"); + my ($kernel, $self, $publicEvents) = @_[ KERNEL, OBJECT, ARG0 ]; + $self->debug("Starting Spectre scheduler."); + my $serviceName = "cron"; + $kernel->alias_set($serviceName); + $kernel->call( IKC => publish => $serviceName, $publicEvents ); + $kernel->yield("checkSchedules"); } #------------------------------------------------------------------- @@ -46,9 +47,9 @@ Gracefully shuts down the scheduler. =cut sub _stop { - my ($kernel, $self) = @_[KERNEL, OBJECT]; - $self->debug("Stopping the scheduler."); - undef $self; + my ($kernel, $self) = @_[KERNEL, OBJECT]; + $self->debug("Stopping the scheduler."); + undef $self; } #------------------------------------------------------------------- @@ -318,7 +319,27 @@ sub getJob { #------------------------------------------------------------------- -=head3 getLogger ( ) +=head2 getJsonStatus ( ) + +Returns JSON of the jobs. + +=cut + +sub getJsonStatus { + my ($kernel, $request, $self) = @_[KERNEL,ARG0,OBJECT]; + my ($sitename, $rsvp) = @$request; + my %data = (); + for my $key (keys %{ $self->{_jobs} }) { + next unless $self->{_jobs}->{$key}->{sitename} eq $sitename; + $data{$key} = $self->{_jobs}->{$key}; + } + $kernel->call(IKC => post => $rsvp, objToJson(\%data)); +} + + +#------------------------------------------------------------------- + +=head2 getLogger ( ) Returns a reference to the logger. @@ -357,7 +378,7 @@ sub new { my $debug = shift; my $self = {_jobs=>{}, _debug=>$debug, _config=>$config, _logger=>$logger}; bless $self, $class; - my @publicEvents = qw(runJob runJobResponse addJob deleteJob); + my @publicEvents = qw(runJob runJobResponse addJob deleteJob getJsonStatus); POE::Session->create( object_states => [ $self => [qw(_start _stop runJob runJobResponse addJob deleteJob checkSchedules checkSchedule), @publicEvents] ], args=>[\@publicEvents] diff --git a/lib/Spectre/Workflow.pm b/lib/Spectre/Workflow.pm index eedcfa599..1b4b23093 100644 --- a/lib/Spectre/Workflow.pm +++ b/lib/Spectre/Workflow.pm @@ -20,6 +20,7 @@ use HTTP::Cookies; use POE qw(Component::Client::HTTP); use POE::Queue::Array; use Tie::IxHash; +use JSON 'objToJson'; #------------------------------------------------------------------- @@ -177,6 +178,66 @@ sub deleteInstance { #------------------------------------------------------------------- +=head2 editWorkflowPriority ( href ) + +Updates the priority of a given workflow instance. + +=head3 href + +Contains information about the instance and the new priority. + +=head4 instanceId + +The id of the instance to update. + +=head4 newPriority + +The new priority value. + +=cut + +sub editWorkflowPriority { + my ($self, $request, $kernel, $session ) = @_[OBJECT, ARG0, KERNEL, SESSION]; + my ($argsHref, $rsvp) = @$request; + + my $instanceId = $argsHref->{instanceId}; + my $newPriority = $argsHref->{newPriority}; + + $self->debug("Updating the priority of $instanceId to $newPriority."); + + # I'm guessing that the payload can't change queues on us + my $found = 0; + my $filterCref = sub { shift->{instanceId} eq $instanceId }; + for my $getQueueMethod (map "get${_}Queue", qw( Suspended Waiting Running )) { + my $q = $self->$getQueueMethod; + my($itemAref) = $q->peek_items($filterCref); # there should be only one + + next unless (ref $itemAref eq 'ARRAY' and @$itemAref); + + my($priority, $id, $payload) = @$itemAref; + my $ackPriority = $q->set_priority($id, $filterCref, $newPriority); + if ($ackPriority != $newPriority) { + # return an error + my $error = 'edit priority setting error'; + $kernel->call(IKC=>post=>$rsvp, objToJson({message => $error})); + } + $found = 1; + last; + } + + if (! $found) { + # return an error message + my $error = 'edit priority instance not found error'; + $kernel->call(IKC=>post=>$rsvp, objToJson({message => $error})); + } + else { + # return success message + $kernel->call(IKC=>post=>$rsvp, objToJson({message => 'edit priority success'})); + } +} + +#------------------------------------------------------------------- + =head2 error ( output ) Prints out error information if debug is enabled. @@ -198,6 +259,44 @@ sub error { #------------------------------------------------------------------- +=head2 getJsonStatus ( ) + +Returns JSON report about the workflow engine. + +=cut + +sub getJsonStatus { + my ($kernel, $request, $self) = @_[KERNEL,ARG0,OBJECT]; + my ($sitename, $rsvp) = @$request; + + # only return this site's info + return $kernel->call(IKC=>post=>$rsvp, '{}') unless $sitename; + + my %queues = (); + tie %queues, 'Tie::IxHash'; + %queues = ( + Suspended => $self->getSuspendedQueue, + Waiting => $self->getWaitingQueue, + Running => $self->getRunningQueue, + ); + my %output = (); + foreach my $queueName (keys %queues) { + my $queue = $queues{$queueName}; + my $count = $queue->get_item_count; + my @instances; + if ($count > 0) { + foreach my $itemAref ($queue->peek_items(sub { shift()->{sitename} eq $sitename })) { + push @instances, $itemAref; + } + } + $output{$queueName} = \@instances; + } + + $kernel->call(IKC=>post=>$rsvp, objToJson(\%output)); +} + +#------------------------------------------------------------------- + =head2 getLogger ( ) Returns a reference to the logger. @@ -340,7 +439,7 @@ sub new { my $debug = shift; my $self = {_debug=>$debug, _config=>$config, _logger=>$logger}; bless $self, $class; - my @publicEvents = qw(addInstance deleteInstance getStatus); + my @publicEvents = qw(addInstance deleteInstance editWorkflowPriority getStatus getJsonStatus); POE::Session->create( object_states => [ $self => [qw(_start _stop returnInstanceToRunnableState addInstance checkInstances deleteInstance suspendInstance runWorker workerResponse), @publicEvents] ], args=>[\@publicEvents] @@ -431,10 +530,11 @@ Suspends a workflow instance for a number of seconds defined in the config file, =cut sub suspendInstance { - my ($self, $instance, $kernel) = @_[OBJECT, ARG0, KERNEL]; - $self->debug("Suspending workflow instance ".$instance->{instanceId}." for ".$self->config->get("suspensionDelay")." seconds."); - $self->getSuspendedQueue->enqueue("1", $instance); - $kernel->delay_set("returnInstanceToRunnableState",$self->config->get("suspensionDelay"), $instance); + my ($self, $instance, $kernel) = @_[OBJECT, ARG0, KERNEL]; + $self->debug("Suspending workflow instance ".$instance->{instanceId}." for ".$self->config->get("suspensionDelay")." seconds."); + my $priority = ($instance->{priority} - 1) * 10; + $self->getSuspendedQueue->enqueue($priority, $instance); + $kernel->delay_set("returnInstanceToRunnableState",$self->config->get("suspensionDelay"), $instance); } #------------------------------------------------------------------- diff --git a/lib/WebGUI/AdminConsole.pm b/lib/WebGUI/AdminConsole.pm index d52e5a542..cdcb7fcf7 100644 --- a/lib/WebGUI/AdminConsole.pm +++ b/lib/WebGUI/AdminConsole.pm @@ -182,6 +182,15 @@ sub getAdminFunction { my $self = shift; my $id = shift; my $functions = { # at some point in the future we'll need to make this pluggable/configurable + "spectre"=>{ + title=>{ + id=>"spectre", + namespace=>"Spectre" + }, + icon=>"spectre.gif", + op=>"spectreStatus", + group=>"3" + }, "assets"=>{ title=>{ id=>"assets", diff --git a/lib/WebGUI/Operation.pm b/lib/WebGUI/Operation.pm index c85d0b041..6ce8d47bf 100644 --- a/lib/WebGUI/Operation.pm +++ b/lib/WebGUI/Operation.pm @@ -227,6 +227,7 @@ sub getOperations { 'spectreGetSiteData' => 'WebGUI::Operation::Spectre', 'spectreTest' => 'WebGUI::Operation::Spectre', + 'spectreStatus' => 'WebGUI::Operation::Spectre', 'ssoViaSessionId' => 'WebGUI::Operation::SSO', diff --git a/lib/WebGUI/Operation/Spectre.pm b/lib/WebGUI/Operation/Spectre.pm index 2c3d0a724..7eebeccd0 100644 --- a/lib/WebGUI/Operation/Spectre.pm +++ b/lib/WebGUI/Operation/Spectre.pm @@ -81,6 +81,70 @@ sub www_spectreGetSiteData { return JSON::objToJson(\%siteData,{autoconv=>0, skipinvalid=>1}); } +#------------------------------------------------------------------- + +=head2 www_spectreStatus ( ) + +Show information about Spectre's current workload. + +=cut + +sub www_spectreStatus { + my $session = shift; + + return $session->privilege->adminOnly() unless $session->user->isInGroup(3); + + # start to prepare the display + my $ac = WebGUI::AdminConsole->new($session, 'spectre'); + my $i18n = WebGUI::International->new($session, 'Spectre'); + + $session->http->setCacheControl("none"); + unless (isInSubnet($session->env->get("REMOTE_ADDR"), $session->config->get("spectreSubnets"))) { + $session->errorHandler->security("make a Spectre workflow runner request, but we're only allowed to accept requests from ".join(",",@{$session->config->get("spectreSubnets")})."."); + return "subnet"; + } + + my $remote = create_ikc_client( + port=>$session->config->get("spectrePort"), + ip=>$session->config->get("spectreIp"), + name=>rand(100000), + timeout=>10 + ); + + if (!$remote) { + return $ac->render($i18n->get('not running'), $i18n->get('spectre')); + } + + my $sitename = $session->config()->get('sitename')->[0]; + my $workflowResult = $remote->post_respond('workflow/getJsonStatus',$sitename); + if (!$workflowResult) { + $remote->disconnect(); + return $ac->render($i18n->get('workflow status error'), $i18n->get('spectre')); + } + + my $cronResult = $remote->post_respond('cron/getJsonStatus',$sitename); + if (! defined $cronResult) { + $remote->disconnect(); + return $ac->render($i18n->get('cron status error'), $i18n->get('spectre')); + } + + my %data = ( + workflow => jsonToObj($workflowResult), + cron => jsonToObj($cronResult), + ); + + my $workflowCount = @{ $data{workflow}{Suspended} } + @{ $data{workflow}{Waiting} } + @{ $data{workflow}{Running} }; + my $workflowUrl = $session->url->page('op=showRunningWorkflows'); + my $cronCount = keys %{ $data{cron} }; + my $cronUrl = $session->url->page('op=manageCron'); + + my $output = $i18n->get('running').'
'; + $output .= sprintf $i18n->get('workflow header'), $workflowUrl, $workflowCount; + $output .= sprintf $i18n->get('cron header'), $cronUrl, $cronCount; + + return $ac->render($output, $i18n->get('spectre')); +} + #------------------------------------------------------------------- =head2 www_spectreTest ( ) diff --git a/lib/WebGUI/Operation/Workflow.pm b/lib/WebGUI/Operation/Workflow.pm index df46776ae..8fdb13d50 100644 --- a/lib/WebGUI/Operation/Workflow.pm +++ b/lib/WebGUI/Operation/Workflow.pm @@ -19,6 +19,8 @@ use WebGUI::Workflow; use WebGUI::Workflow::Activity; use WebGUI::Workflow::Instance; use WebGUI::Utility; +use POE::Component::IKC::ClientLite; +use JSON 'jsonToObj'; =head1 NAME @@ -231,6 +233,60 @@ sub www_editWorkflow { return $ac->render($f->print.$addmenu.$steps, 'edit workflow'); } +#------------------------------------------------------------------- + +=head2 www_editWorkflowPriority ( ) + +Save the submitted new workflow priority. + +=cut + +sub www_editWorkflowPriority { + my $session = shift; + + return $session->privilege->insufficient() unless $session->user->isInGroup(3); + + my $i18n = WebGUI::International->new($session, 'Workflow'); + my $ac = WebGUI::AdminConsole->new($session,"workflow"); + $ac->addSubmenuItem($session->url->page("op=showRunningWorkflows"), $i18n->get('show running workflows')); + $ac->setHelp('manage workflows', 'Workflow'); + + # make sure the input is good + my $instanceId = $session->form->get('instanceId') || ''; + my $newPriority = $session->form->get('newPriority') || ''; + if (! $instanceId) { + my $output = $i18n->get('edit priority bad request'); + return $ac->render($output, $i18n->get('show running workflows')); + } + + # make the request + my $remote = create_ikc_client( + port=>$session->config->get("spectrePort"), + ip=>$session->config->get("spectreIp"), + name=>rand(100000), + timeout=>10 + ); + if (! $remote) { + my $output = $i18n->get('edit priority no spectre error'); + return $ac->render($output, $i18n->get('show running workflows')); + } + + my $argHref = { + instanceId => $instanceId, + newPriority => $newPriority, + }; + my $resultJson = $remote->post_respond('workflow/editWorkflowPriority', $argHref); + if (! defined $resultJson) { + $remote->disconnect(); + my $output = $i18n->get('edit priority no info error'); + return $ac->render($output, $i18n->get('show running workflows')); + } + + my $responseHref = jsonToObj($resultJson); + + my $message = $i18n->get($responseHref->{message}) || $i18n->get('edit priority unknown error'); + return $ac->render($message, $i18n->get('show running workflows')); +} #------------------------------------------------------------------- @@ -397,39 +453,132 @@ Display a list of the running workflow instances. =cut sub www_showRunningWorkflows { - my $session = shift; - return $session->privilege->insufficient() unless ($session->user->isInGroup("pbgroup000000000000015")); - my $i18n = WebGUI::International->new($session, "Workflow"); - my $output = ''; - my $isAdmin = $session->user->isInGroup("3"); - my $rs = $session->db->read("select Workflow.title, WorkflowInstance.lastStatus, WorkflowInstance.runningSince, WorkflowInstance.lastUpdate, WorkflowInstance.instanceId from WorkflowInstance left join Workflow on WorkflowInstance.workflowId=Workflow.workflowId order by WorkflowInstance.runningSince desc"); - while (my ($title, $status, $runningSince, $lastUpdate, $id) = $rs->array) { - my $class = $status || "complete"; - $output .= '' - .'' - .''; - if ($status) { - $output .= ''; - } - $output .= '' if ($isAdmin); - $output .= "\n"; - } - $output .= '
'.$title.''.$session->datetime->epochToHuman($runningSince).'' - .$status.' / '.$session->datetime->epochToHuman($lastUpdate) - .''.$i18n->get("run").'
'; - my $ac = WebGUI::AdminConsole->new($session,"workflow"); - $ac->addSubmenuItem($session->url->page("op=addWorkflow"), $i18n->get("add a new workflow")); - $ac->addSubmenuItem($session->url->page("op=manageWorkflows"), $i18n->get("manage workflows")); - $ac->setHelp('show running workflows', 'Workflow'); - return $ac->render($output, 'show running workflows'); + my $session = shift; + + return $session->privilege->insufficient() unless ($session->user->isInGroup("pbgroup000000000000015")); + + my $i18n = WebGUI::International->new($session, "Workflow"); + my $ac = WebGUI::AdminConsole->new($session,"workflow"); + my $isAdmin = $session->user->isInGroup("3"); + + # javascript for creating/showing/hiding the edit priority form + my $cancel = $i18n->get('edit priority cancel'); + my $updatePriority = $i18n->get('edit priority update priority'); + my $output = <<"ENDCODE"; + + +ENDCODE + + my $remote = create_ikc_client( + port=>$session->config->get("spectrePort"), + ip=>$session->config->get("spectreIp"), + name=>rand(100000), + timeout=>10 + ); + if (! $remote) { + my $output = $i18n->get('spectre not running error'); + return $ac->render($output, $i18n->get('show running workflows')); + } + + my $sitename = $session->config()->get('sitename')->[0]; + my $workflowResult = $remote->post_respond('workflow/getJsonStatus',$sitename); + if (! defined $workflowResult) { + $remote->disconnect(); + my $output = $i18n->get('spectre no info error'); + return $ac->render($output, $i18n->get('show running workflows')); + } + + my $workflowsHref = jsonToObj($workflowResult); + + my $workflowTitleFor = $session->db->buildHashRef(<<""); + SELECT wi.instanceId, w.title + FROM WorkflowInstance wi + JOIN Workflow w USING (workflowId) + + my $lastActivityFor = $session->db->buildHashRef(<<""); + SELECT wi.instanceId, wa.title + FROM WorkflowInstance wi + JOIN WorkflowActivity wa ON wi.currentActivityId = wa.activityId + + for my $workflowType (qw( Suspended Waiting Running )) { + my $workflowsAref = $workflowsHref->{$workflowType}; + my $workflowCount = @$workflowsAref; + + my $titleHeader = $i18n->get('title header'); + my $priorityHeader = $i18n->get('priority header'); + my $activityHeader = $i18n->get('activity header'); + my $lastStateHeader = $i18n->get('last state header'); + my $lastRunTimeHeader = $i18n->get('last run time header'); + $output .= sprintf $i18n->get('workflow type count'), $workflowCount, $workflowType; + $output .= ''; + $output .= ""; + $output .= ""; + + for my $workflow (@$workflowsAref) { + my($priority, $id, $instance) = @$workflow; + + my $originalPriority = ($instance->{priority} - 1) * 10; + my $instanceId = $instance->{instanceId}; + my $title = $workflowTitleFor->{$instanceId} || '(no title)'; + my $lastActivity = $lastActivityFor->{$instanceId} || '(none)'; + my $lastRunTime = $instance->{lastRunTime} || '(never)'; + + $output .= ''; + $output .= ""; + $output .= qq[]; + $output .= ""; + $output .= ""; + $output .= ""; + + if ($isAdmin) { + my $run = $i18n->get('run'); + my $href = $session->url->page(qq[op=runWorkflow;instanceId=$instanceId]); + $output .= qq[]; + } + $output .= "\n"; + } + $output .= '
$titleHeader$priorityHeader$activityHeader$lastStateHeader$lastRunTimeHeader
$title$priority/$originalPriority$lastActivity$instance->{lastState}$lastRunTime$run
'; + } + + $ac->addSubmenuItem($session->url->page("op=addWorkflow"), $i18n->get("add a new workflow")); + $ac->addSubmenuItem($session->url->page("op=manageWorkflows"), $i18n->get("manage workflows")); + $ac->setHelp('show running workflows', 'Workflow'); + + return $ac->render($output, 'show running workflows'); } - 1; diff --git a/lib/WebGUI/i18n/English/Spectre.pm b/lib/WebGUI/i18n/English/Spectre.pm new file mode 100644 index 000000000..91b488479 --- /dev/null +++ b/lib/WebGUI/i18n/English/Spectre.pm @@ -0,0 +1,58 @@ +package WebGUI::i18n::English::Spectre; ##Be sure to change the package name to match the filename + +our $I18N = { ##hashref of hashes + 'spectre' => { + message => q|Spectre|, + lastUpdated => 0, + context => q||, + }, + + 'running' => { + message => q|Spectre is running.|, + lastUpdated => 0, + context => q|let the user know that spectre's off| + }, + + 'not running' => { + message => q|Spectre is not running.|, + lastUpdated => 0, + context => q|let the user know that spectre's off| + }, + + 'workflow status error' => { + message => q|Spectre is running, but there was an error getting the workflow status.|, + lastUpdated => 0, + context => q||, + }, + + 'cron status error' => { + message => q|Spectre is running, but there was an error getting the cron status.|, + lastUpdated => 0, + context => q||, + }, + + 'workflow header' => { + message => q|There are %d workflows.
|, + lastUpdated => 0, + context => q||, + }, + + 'cron header' => { + message => q|There are %d scheduled tasks|, + lastUpdated => 0, + context => q||, + }, + + #If the help file documents an Asset, it must include an assetName key + #If the help file documents an Macro, it must include an macroName key + #If the help file documents a Workflow Activity, it must include an activityName key + #If the help file documents a Template Parser, it must include an templateParserName key + #For all other types, use topicName + 'assetName' => { + message => q|This should not matter...?|, + lastUpdated => 1131394072, + }, + +}; + +1; diff --git a/lib/WebGUI/i18n/English/Workflow.pm b/lib/WebGUI/i18n/English/Workflow.pm index f3b404a0e..36a20a70c 100644 --- a/lib/WebGUI/i18n/English/Workflow.pm +++ b/lib/WebGUI/i18n/English/Workflow.pm @@ -164,8 +164,9 @@ our $I18N = { 'show running workflows body' => { message => q| -

This screen can help you debug problems with workflows by showing which workflows are currently running. The workflows are shown in a table with the name of the workflow, the date it started running. If the workflow has a defined status, then that status will also be shown, along with the date the workflow's status was last updated.

-

The screen will not automatically update. To update the list of running workflows, reload the page.

+

This screen can help you debug problems with workflows by showing which workflows are currently running. The workflows are grouped by status, with their names, current and original priorities, current activities (if any), last state, and when the workflow was last run.

+

You can edit the priority of workflows by clicking on the priority links and submitting the form that appears. You can also run a workflow by clicking the "Run" link in the right column of the table, if present.

+

The screen will not automatically update. To update the list of running workflows, reload the page.

|, lastUpdated => 1151719633, }, @@ -195,6 +196,108 @@ and add activities to it.

lastUpdated => 1151721687, }, + 'edit priority success' => { + message => q|Workflow priority updated successfully.|, + context => q||, + lastUpdated => 0, + }, + + 'edit priority instance not found error' => { + message => q|I could not find that workflow. Perhaps it's finished running.|, + context => q||, + lastUpdated => 0, + }, + + 'edit priority cancel' => { + message => q|cancel|, + context => q||, + lastUpdated => 0, + }, + + 'edit priority update priority' => { + message => q|Update Priority|, + context => q||, + lastUpdated => 0, + }, + + 'spectre not running error' => { + message => q|Spectre is not running.
Unable to get workflow information.|, + context => q||, + lastUpdated => 0, + }, + + 'spectre no info error' => { + message => q|Spectre is running, but I was not able to get workflow information.|, + context => q||, + lastUpdated => 0, + }, + + 'workflow type count' => { + message => q|

%d %s Workflows

|, + context => q||, + lastUpdated => 0, + }, + + 'title header' => { + message => q|Title|, + context => q||, + lastUpdated => 0, + }, + + 'priority header' => { + message => q|Current/Original Priority|, + context => q||, + lastUpdated => 0, + }, + + 'activity header' => { + message => q|Current Activity|, + context => q||, + lastUpdated => 0, + }, + + 'last state header' => { + message => q|Last State|, + context => q||, + lastUpdated => 0, + }, + + 'last run time header' => { + message => q|Last Run Time|, + context => q||, + lastUpdated => 0, + }, + + 'edit priority setting error' => { + message => q|There was an error setting the new priority.|, + context => q||, + lastUpdated => 0, + }, + + 'edit priority no spectre error' => { + message => q|Spectre is not running.
Unable to get workflow information.|, + context => q||, + lastUpdated => 0, + }, + + 'edit priority bad request' => { + message => q|You have made a bad request.|, + context => q||, + lastUpdated => 0, + }, + + 'edit priority no info error' => { + message => q|Spectre is running, but I was not able to update the priority.|, + context => q||, + lastUpdated => 0, + }, + + 'edit priority unknown error' => { + message => q|There was an unknown error updating the workflow priority. Please try again later.|, + context => q||, + lastUpdated => 0, + }, + 'topicName' => { message => q|Workflow|, context => q|The title of the workflow interface.|, diff --git a/www/extras/adminConsole/small/spectre.gif b/www/extras/adminConsole/small/spectre.gif new file mode 100644 index 0000000000000000000000000000000000000000..12be04ff913aa6bd253bc7d2f54f56712de4e75d GIT binary patch literal 587 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 literal 0 HcmV?d00001