Skip to content

Commit 4c421fa

Browse files
authored
Jobs delete button (#39)
* Jobs delete button * Delete job output file on job delete
1 parent 3776e9a commit 4c421fa

File tree

10 files changed

+208
-9
lines changed

10 files changed

+208
-9
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to `cybercog/laravel-paket` will be documented in this file.
44

55
## [Unreleased]
66

7+
### Added
8+
9+
- ([#39]) Add jobs deletion
10+
711
## [1.3.0]
812

913
### Added
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<template>
2+
<div class="relative">
3+
<button :class="getOptionsButtonClass()" v-on:click="toggleOptionsMenu()">
4+
<svg aria-label="Show options" viewBox="0 0 13 16" version="1.1" width="13" height="16" role="img">
5+
<path fill-rule="evenodd" d="M1.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm5 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM13 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"></path>
6+
</svg>
7+
</button>
8+
<div class="absolute bg-white border rounded right-0 shadow text-sm whitespace-no-wrap" v-if="isMenuOpened">
9+
<button class="hover:bg-red-300 py-2 px-3" v-on:click="confirmDeleteJob()">Delete Job</button>
10+
</div>
11+
</div>
12+
</template>
13+
14+
<script>
15+
import Swal from 'sweetalert2';
16+
17+
export default {
18+
props: {
19+
job: {
20+
type: Object,
21+
required: true,
22+
},
23+
},
24+
25+
data() {
26+
return {
27+
isMenuOpened: false,
28+
};
29+
},
30+
31+
methods: {
32+
getOptionsButtonClass() {
33+
const classes = 'p-2 hover:bg-gray-200 rounded-full';
34+
35+
return classes + ` ${this.isMenuOpened ? 'bg-gray-200' : ''}`;
36+
},
37+
38+
toggleOptionsMenu() {
39+
this.isMenuOpened = !this.isMenuOpened;
40+
},
41+
42+
async deleteJob() {
43+
await this.$store.dispatch('deleteJobs', {
44+
id: this.job.id,
45+
});
46+
47+
await this.$store.dispatch('collectJobs');
48+
},
49+
50+
async confirmDeleteJob() {
51+
this.toggleOptionsMenu();
52+
53+
const response = await Swal.fire({
54+
type: 'warning',
55+
title: 'Job Deletion',
56+
text: 'Do you want to delete job permanently?',
57+
showCancelButton: true,
58+
confirmButtonText: 'Delete',
59+
confirmButtonColor: '#f56565',
60+
});
61+
62+
if (response.value) {
63+
this.deleteJob();
64+
65+
await Swal.fire({
66+
type: 'success',
67+
title: 'Job Deleted!',
68+
});
69+
70+
const jobsListUrl = this.$store.getters.getUrl('/jobs');
71+
const currentUrl = window.location.href;
72+
if (currentUrl !== jobsListUrl) {
73+
window.location.href = jobsListUrl;
74+
}
75+
}
76+
},
77+
},
78+
}
79+
</script>

resources/js/screens/jobs/index.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
<div class="rounded overflow-hidden shadow mt-3" v-for="job in getJobs">
66
<div class="p-4">
7-
<div>
7+
<div class="flex">
88
<div class="font-mono" v-text="getCommandLine(job)"></div>
9+
<job-options-menu class="ml-auto mr-3" :job="job"></job-options-menu>
910
</div>
1011
<div class="flex">
1112
<div class="mt-2">
@@ -25,11 +26,13 @@
2526

2627
<script>
2728
import moment from 'moment';
28-
import JobStatusBadge from '../../components/Job/Status/Badge';
29+
import JobStatusBadge from '../../components/Job/StatusBadge';
30+
import JobOptionsMenu from '../../components/Job/OptionsMenu';
2931
3032
export default {
3133
components: {
3234
JobStatusBadge,
35+
JobOptionsMenu,
3336
},
3437
3538
data() {

resources/js/screens/jobs/item.vue

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@
44
<router-link class="text-indigo-700 hover:text-indigo-900" :to="{name: 'jobs'}">Jobs</router-link>
55
</h1>
66

7-
<div class="rounded overflow-hidden shadow mt-3">
7+
<div class="rounded overflow-hidden shadow mt-3 py-3">
88
<div class="flex">
9-
<div class="mt-3 mx-3">
9+
<div class="mx-3">
1010
<h4 class="text-xl font-mono" v-text="job.id"></h4>
1111
</div>
1212

13-
<div class="flex mt-3 ml-auto mr-3">
13+
<div class="flex ml-auto">
1414
<span class="bg-gray-200 border-b-2 border-gray-400 px-2 py-1 text-sm font-semibold font-mono tracking-wide text-gray-700 mr-3" title="Execution start time">
1515
<time v-text="getCreatedAt(job)"></time>
1616
</span>
17-
<job-status-badge :status="getStatus(job)"></job-status-badge>
17+
<job-status-badge class="mr-3" :status="getStatus(job)"></job-status-badge>
18+
<job-options-menu class="mr-3" :job="job"></job-options-menu>
1819
</div>
1920
</div>
2021

21-
<div class="p-4 text-left bg-black mt-4" v-show="processOutput">
22+
<div class="p-4 text-left bg-black mt-4 -mb-3" v-show="processOutput">
2223
<code class="bg-black text-white border-0" v-html="processOutput"></code>
2324
</div>
2425
</div>
@@ -28,11 +29,13 @@
2829
<script>
2930
import AnsiConverter from 'ansi-to-html';
3031
import moment from 'moment';
31-
import JobStatusBadge from '../../components/Job/Status/Badge';
32+
import JobStatusBadge from '../../components/Job/StatusBadge';
33+
import JobOptionsMenu from '../../components/Job/OptionsMenu';
3234
3335
export default {
3436
components: {
3537
JobStatusBadge,
38+
JobOptionsMenu,
3639
},
3740
3841
data() {

resources/js/store.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const actions = {
3838
this.dispatch('collectRequirements');
3939
},
4040

41+
async deleteJobs(context, payload) {
42+
await Axios.delete(this.getters.getUrl(`/api/jobs/${payload.id}`));
43+
},
44+
4145
async collectJobs() {
4246
const response = await Axios.get(this.getters.getUrl('/api/jobs'));
4347

@@ -58,7 +62,7 @@ const getters = {
5862
},
5963

6064
getUrl: () => (uri) => {
61-
return '/' + window.Paket.baseUri + uri;
65+
return window.location.origin + '/' + window.Paket.baseUri + uri;
6266
},
6367

6468
getJob: (state, getters) => (jobId) => {

routes/web.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
Route::get('jobs/{job}')
3030
->uses('Jobs\GetAction')
3131
->name('jobs.get');
32+
33+
Route::delete('jobs/{job}')
34+
->uses('Jobs\DeleteAction')
35+
->name('jobs.delete');
3236
});
3337

3438
Route::get('{view?}', 'AppAction')
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Laravel Paket.
5+
*
6+
* (c) Anton Komarev <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs;
15+
16+
use Cog\Contracts\Paket\Job\Repositories\Job as JobRepositoryContract;
17+
use Illuminate\Contracts\Support\Responsable as ResponsableContract;
18+
19+
final class DeleteAction
20+
{
21+
public function __invoke(string $id, JobRepositoryContract $jobs): ResponsableContract
22+
{
23+
$job = $jobs->getById($id);
24+
$jobs->deleteById($id);
25+
26+
return new DeleteResponse($job);
27+
}
28+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Laravel Paket.
5+
*
6+
* (c) Anton Komarev <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Cog\Laravel\Paket\Http\Controllers\Api\Jobs;
15+
16+
use Cog\Contracts\Paket\Job\Entities\Job as JobContract;
17+
use Illuminate\Contracts\Support\Responsable as ResponsableContract;
18+
use Illuminate\Http\JsonResponse;
19+
20+
final class DeleteResponse implements ResponsableContract
21+
{
22+
private $job;
23+
24+
public function __construct(JobContract $job)
25+
{
26+
$this->job = $job;
27+
}
28+
29+
/**
30+
* Create an HTTP response that represents the object.
31+
*
32+
* @param \Illuminate\Http\Request $request
33+
* @return \Symfony\Component\HttpFoundation\Response
34+
*/
35+
public function toResponse($request)
36+
{
37+
return $this->toJson();
38+
}
39+
40+
private function toJson(): JsonResponse
41+
{
42+
return response()->json($this->job->toArray());
43+
}
44+
}

src/Job/Repositories/JobFileRepository.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,30 @@ public function getById(string $id): JobContract
7676
throw new NotFoundHttpException("Job with id `{$id}` not found.");
7777
}
7878

79+
// TODO: [v2.0] Add to contract
80+
public function deleteById(string $id): void
81+
{
82+
$index = $this->getIndex();
83+
84+
foreach ($index as $key => $job) {
85+
if ($job['id'] === $id) {
86+
$jobKey = $key;
87+
break;
88+
}
89+
}
90+
91+
if (!isset($jobKey)) {
92+
throw new NotFoundHttpException("Job with id `{$id}` not found.");
93+
}
94+
95+
unset($index[$jobKey]);
96+
$index = array_values($index);
97+
98+
$this->deleteJobProcessOutput($id);
99+
100+
$this->putIndex($index);
101+
}
102+
79103
public function store(JobContract $job): void
80104
{
81105
$index = $this->getIndex();
@@ -150,6 +174,12 @@ private function getJobProcessOutput(string $jobId): string
150174
}
151175
}
152176

177+
private function deleteJobProcessOutput(string $jobId): void
178+
{
179+
$path = sprintf('%s/jobs/%s.log', $this->storagePath, $jobId);
180+
$this->files->delete($path);
181+
}
182+
153183
private function getIndexFilePath(): string
154184
{
155185
return $this->storagePath . '/jobs.json';

0 commit comments

Comments
 (0)