At first, we add a little code in the Job model.
We modify default toArray behavior with some extra properties — name and title.
public $definition;public function setDefinition(JobDefinition $definition)
{
$this->definition = $definition;
return $this;
}public function toArray()
{
return [
'id' => $this->id,
'name' => $this->commandName(),
'command' => $this->command,
'title' => optional($this->definition)->title(),
'state' => $this->state,
'payload' => $this->payload,
'progress' => $this->progress,
'report' => $this->report,
'attempts' => $this->attempts,
'owner' => $this->created_by,
'created_at' => $this->created_at->toDateTimeString(),
];
}
and in JobController
public function show(Job $job)
{
$definition = $this->repository
->findByName(class_basename($job->command));
$job->setDefinition($definition);
return view('jobs.show', ['job'=>$job]);
}
… and in routes/web.php
Route::get('/job/{job}/show', 'JobController@show')
->name('jobs.show');
of course we needs views/jobs/show.blade.php
@extends('jobs.master')
@section('jobs_content')
@json($job)
@endsection
it is ultra simple now, only for check valid output, because we will templating it with vue
Next, open resources/views/layouts/app.blade.php and add script for connection to socket.io (6001 port — is a port of laravel-echo-server, if you change it in config file laravel-echo-server.json, you should change it in this place too) in the <head> section. Also we define some user info for use in vue-side
<script src="{{config('app.url')}}:6001/socket.io/socket.io.js"></script>
<script>
@auth()
window.appConfig = {
auth:@json(Auth::user()->toCredentials())
};
@elseauth()
window.appConfig = {
auth:null
};
@endauth
</script>
and in the User model add method
public function toCredentials()
{
return [
'id' => $this->id,
'name' => $this->name
];
}
It`s time to prepare assets
Create new vue component ActiveJob.vue in assets/js/components
It got initial job information in property, and then subscribed on job related channel, After receiving broadcasted data, it update job information, and vue make reactive magic
add in app.js near other imports
import ActiveJob from './components/ActiveJob';
and register component
...
const app = new Vue({
el: '#app',
components:{ActiveJob},
...
in views/jobs/show.blade.php change @json($job)
to
<active-job job='@json($job)'/>
Finally, run in terminal npm run dev
for building scripts
next run in separated terminal window laravel-echo-server
and try to run any commands from site. Be free for modifying DummyJob, or other for simulate job progress and notifications. As more real example I write this job
Just one sensitive thing about logs. We have action in JobLogController for show logs by id. But we haven’t any way to know logs id from jobs.show action. So we need to create action for show job execution log by loggable_type and loggable_id , those equals $job->id and $job->commandName()
public function showGroup($type, $id)
{
$log = JobLog::where(
['loggable_type' => $type, 'loggable_id' => $id]
)->firstOrFail();
$logs = JobLog::job($log)->paginate();
return view('logs.show', ['logs' => $logs, 'group' => $log]);
}//Route::get('/logs/{type}/{id}', 'JobLogController@showGroup');
Well, now we can show progress for current task, but what about monitoring all active tasks?
Let`s go to add watch action in JobController
public function watch()
{
$jobs = Job::notFinished()->get()->toJson();
return view('jobs.watch', ['jobs' => $jobs]);
}
we add notFinished scope in Job model
public function scopeNotFinished(Builder $query)
{
return $query->whereNotIn('state', ['fail', 'success']);
}
/views/jobs/watch.blade.php will be simple, too
@extends('jobs.master')
@section('jobs_content')
<watch-active list="{{$jobs}}"/>
@endsection
Now make WatchActive.vue component. It is simple list collection of ActivityJob components. It listen information about newly created jobs from JobsMonitor channel, and pushed new job in list, where it represented by ActiveJob component
We add “single” property into ActiveJob.vue, for mark if it as standalone component, otherwise we want to show button for remove item from list and unsubscribe from job channel
so updates for ActiveJob.vue in template section
....
<div class="card-header">
<span v-html="label"></span>
{{job.title}} [{{job.command}}: {{job.id}}]
<a class="float-right"
@click="remove(job.id)"
v-show="single !== 1">
<i class="fa fa-times-circle-o"></i>
</a>
</div>
....
and in script section
<script>
export default {
props: {
job: {},
isSingle: 1
},
....
methods: {
remove(id){
this.$echo.leave(`Job.${this.job.id}`);
this.$nextTick(function () {
this.$emit('removeItem', id);
});
},
....
Now we needs new broadcasting channel JobsMonitor
Broadcast::channel('JobsMonitor.{id}', function (User $user, $id) {
return (int)$user->id === (int)$id;
});
And new Notification about creating new Job
We apply it in the JobModelObserver class
...
public function created(Job $job)
{
try{
$command = app()->make($job->command, ['jobModel'=>$job]);
dispatch($command)->onQueue($job->queue)->delay(2);
$job->notifyNow(new JobCreatedMessage());
}catch (\Throwable $e){
$job->delete();
}
}
JobsMonitor channel is private, that means that each user can see only own activity. Permission improvements, ability for retry and decline jobs may comes in next part, If this series get positive feedback
You can find demo application here https://github.com/Insolita/InteractiveJobs
That`s all