Organize Realtime Job Management With Laravel and Vue Step by Step [Part 3]

<- previous part

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