add log viewer for queues

This commit is contained in:
Saeed Vaziry 2024-03-16 12:20:32 +01:00
parent fd93f3dd47
commit 7a6dcb5654
13 changed files with 163 additions and 27 deletions

View File

@ -0,0 +1,13 @@
<?php
namespace App\Actions\Queue;
use App\Models\Queue;
class GetQueueLogs
{
public function getLogs(Queue $queue): string
{
return $queue->server->processManager()->handler()->getLogs($queue->getLogFile());
}
}

View File

@ -4,6 +4,7 @@
use App\Actions\Queue\CreateQueue; use App\Actions\Queue\CreateQueue;
use App\Actions\Queue\DeleteQueue; use App\Actions\Queue\DeleteQueue;
use App\Actions\Queue\GetQueueLogs;
use App\Actions\Queue\ManageQueue; use App\Actions\Queue\ManageQueue;
use App\Facades\Toast; use App\Facades\Toast;
use App\Helpers\HtmxResponse; use App\Helpers\HtmxResponse;
@ -51,4 +52,9 @@ public function destroy(Server $server, Site $site, Queue $queue): RedirectRespo
return back(); return back();
} }
public function logs(Server $server, Site $site, Queue $queue): RedirectResponse
{
return back()->with('content', app(GetQueueLogs::class)->getLogs($queue));
}
} }

35
package-lock.json generated
View File

@ -20,6 +20,7 @@
"prettier-plugin-tailwindcss": "^0.5.11", "prettier-plugin-tailwindcss": "^0.5.11",
"pusher-js": "^4.3.1", "pusher-js": "^4.3.1",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7",
"toastr": "^2.1.4", "toastr": "^2.1.4",
"vite": "^4.5.2" "vite": "^4.5.2"
} }
@ -465,6 +466,16 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@ryangjchandler/alpine-clipboard": { "node_modules/@ryangjchandler/alpine-clipboard": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ryangjchandler/alpine-clipboard/-/alpine-clipboard-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@ryangjchandler/alpine-clipboard/-/alpine-clipboard-2.2.0.tgz",
@ -1879,6 +1890,15 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.9.0"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -2294,6 +2314,12 @@
"fastq": "^1.6.0" "fastq": "^1.6.0"
} }
}, },
"@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"dev": true
},
"@ryangjchandler/alpine-clipboard": { "@ryangjchandler/alpine-clipboard": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ryangjchandler/alpine-clipboard/-/alpine-clipboard-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@ryangjchandler/alpine-clipboard/-/alpine-clipboard-2.2.0.tgz",
@ -3225,6 +3251,15 @@
"thenify": ">= 3.1.0 < 4" "thenify": ">= 3.1.0 < 4"
} }
}, },
"tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"dev": true,
"requires": {
"@popperjs/core": "^2.9.0"
}
},
"to-regex-range": { "to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@ -22,6 +22,7 @@
"prettier-plugin-tailwindcss": "^0.5.11", "prettier-plugin-tailwindcss": "^0.5.11",
"pusher-js": "^4.3.1", "pusher-js": "^4.3.1",
"tailwindcss": "^3.1.0", "tailwindcss": "^3.1.0",
"tippy.js": "^6.3.7",
"toastr": "^2.1.4", "toastr": "^2.1.4",
"vite": "^4.5.2" "vite": "^4.5.2"
} }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,15 @@
"isEntry": true, "isEntry": true,
"src": "resources/css/app.css" "src": "resources/css/app.css"
}, },
"resources/js/app.css": {
"file": "assets/app-a1ae07b3.css",
"src": "resources/js/app.css"
},
"resources/js/app.js": { "resources/js/app.js": {
"file": "assets/app-c41c626e.js", "css": [
"assets/app-a1ae07b3.css"
],
"file": "assets/app-44a1ce19.js",
"isEntry": true, "isEntry": true,
"src": "resources/js/app.js" "src": "resources/js/app.js"
} }

View File

@ -1,5 +1,4 @@
@import "toastr.css"; @import "toastr.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@ -1,4 +1,4 @@
import Alpine from 'alpinejs'; import tippy from 'tippy.js';
Alpine.directive('clipboard', (el) => { Alpine.directive('clipboard', (el) => {
let text = el.textContent let text = el.textContent
@ -54,3 +54,18 @@ window.toastr.options = {
"positionClass": "toast-bottom-right", "positionClass": "toast-bottom-right",
"preventDuplicates": true, "preventDuplicates": true,
} }
import 'tippy.js/dist/tippy.css';
import Alpine from 'alpinejs';
document.body.addEventListener('htmx:afterSettle', (event) => {
tippy('[data-tooltip]', {
content(reference) {
return reference.getAttribute('data-tooltip');
},
});
});
tippy('[data-tooltip]', {
content(reference) {
return reference.getAttribute('data-tooltip');
},
});

View File

@ -27,27 +27,41 @@
hx-post="{{ route('servers.sites.queues.action', ['server' => $server, 'site' => $site, 'queue' => $queue, 'action' => 'stop']) }}" hx-post="{{ route('servers.sites.queues.action', ['server' => $server, 'site' => $site, 'queue' => $queue, 'action' => 'stop']) }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-select="#queue-actions-{{ $queue->id }}" hx-select="#queue-actions-{{ $queue->id }}"
data-tooltip="Stop"
> >
Stop <x-heroicon-o-stop class="h-5 w-5" />
</x-icon-button> </x-icon-button>
<x-icon-button <x-icon-button
id="resume-{{ $queue->id }}" id="resume-{{ $queue->id }}"
hx-post="{{ route('servers.sites.queues.action', ['server' => $server, 'site' => $site, 'queue' => $queue, 'action' => 'start']) }}" hx-post="{{ route('servers.sites.queues.action', ['server' => $server, 'site' => $site, 'queue' => $queue, 'action' => 'start']) }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-select="#queue-actions-{{ $queue->id }}" hx-select="#queue-actions-{{ $queue->id }}"
data-tooltip="Start"
> >
Start <x-heroicon-o-play class="h-5 w-5" />
</x-icon-button> </x-icon-button>
<x-icon-button <x-icon-button
id="restart-{{ $queue->id }}" id="restart-{{ $queue->id }}"
hx-post="{{ route('servers.sites.queues.action', ['server' => $server, 'site' => $site, 'queue' => $queue, 'action' => 'restart']) }}" hx-post="{{ route('servers.sites.queues.action', ['server' => $server, 'site' => $site, 'queue' => $queue, 'action' => 'restart']) }}"
hx-swap="outerHTML" hx-swap="outerHTML"
hx-select="#queue-actions-{{ $queue->id }}" hx-select="#queue-actions-{{ $queue->id }}"
data-tooltip="Restart"
> >
Restart <x-heroicon-o-arrow-path class="h-5 w-5" />
</x-icon-button>
<x-icon-button
id="logs-{{ $queue->id }}"
x-on:click="$dispatch('open-modal', 'show-log'); document.getElementById('log-content').firstChild.innerHTML = '';"
hx-get="{{ route('servers.sites.queues.logs', ['server' => $server, 'site' => $site, 'queue' => $queue]) }}"
hx-target="#log-content"
hx-select="#log-content"
data-tooltip="Logs"
>
<x-heroicon-o-square-3-stack-3d class="h-5 w-5" />
</x-icon-button> </x-icon-button>
<x-icon-button <x-icon-button
x-on:click="deleteAction = '{{ route('servers.sites.queues.destroy', ['server' => $server, 'site' => $site, 'queue' => $queue]) }}'; $dispatch('open-modal', 'delete-queue')" x-on:click="deleteAction = '{{ route('servers.sites.queues.destroy', ['server' => $server, 'site' => $site, 'queue' => $queue]) }}'; $dispatch('open-modal', 'delete-queue')"
data-tooltip="Delete"
> >
<x-heroicon-o-trash class="h-5 w-5" /> <x-heroicon-o-trash class="h-5 w-5" />
</x-icon-button> </x-icon-button>
@ -71,4 +85,25 @@
method="delete" method="delete"
x-bind:action="deleteAction" x-bind:action="deleteAction"
/> />
<x-modal name="show-log" max-width="4xl">
<div class="p-6">
<h2 class="mb-5 text-lg font-medium text-gray-900 dark:text-gray-100">
{{ __("View Log") }}
</h2>
<div id="log-content">
<x-console-view>
@if (session()->has("content"))
{{ session("content") }}
@else
Loading...
@endif
</x-console-view>
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button type="button" x-on:click="$dispatch('close')">
{{ __("Close") }}
</x-secondary-button>
</div>
</div>
</x-modal>
</div> </div>

View File

@ -54,6 +54,7 @@
Route::post('/{site}/queues', [QueueController::class, 'store'])->name('servers.sites.queues.store'); Route::post('/{site}/queues', [QueueController::class, 'store'])->name('servers.sites.queues.store');
Route::post('/{site}/queues/{queue}/action/{action}', [QueueController::class, 'action'])->name('servers.sites.queues.action'); Route::post('/{site}/queues/{queue}/action/{action}', [QueueController::class, 'action'])->name('servers.sites.queues.action');
Route::delete('/{site}/queues/{queue}', [QueueController::class, 'destroy'])->name('servers.sites.queues.destroy'); Route::delete('/{site}/queues/{queue}', [QueueController::class, 'destroy'])->name('servers.sites.queues.destroy');
Route::get('/{site}/queues/{queue}/logs', [QueueController::class, 'logs'])->name('servers.sites.queues.logs');
// site settings // site settings
Route::get('/{site}/settings', [SiteSettingController::class, 'index'])->name('servers.sites.settings'); Route::get('/{site}/settings', [SiteSettingController::class, 'index'])->name('servers.sites.settings');

View File

@ -167,4 +167,27 @@ public function test_restart_queue(): void
'status' => QueueStatus::RUNNING, 'status' => QueueStatus::RUNNING,
]); ]);
} }
public function test_show_logs(): void
{
SSH::fake('logs');
$this->actingAs($this->user);
$queue = Queue::factory()->create([
'server_id' => $this->server->id,
'site_id' => $this->site->id,
'status' => QueueStatus::RUNNING,
]);
$this->get(
route('servers.sites.queues.logs', [
'server' => $this->server,
'site' => $this->site,
'queue' => $queue,
])
)
->assertSessionDoesntHaveErrors()
->assertSessionHas('content', 'logs');
}
} }