Add button to view install logs (#1356)

Co-authored-by: notCharles <charles@pelican.dev>
This commit is contained in:
Boy132 2025-05-09 21:03:32 +02:00 committed by GitHub
parent 7971dc13fc
commit 2296e41a8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 240 additions and 6 deletions

View File

@ -2,7 +2,7 @@
namespace App\Filament\Admin\Resources\ServerResource\Pages;
use App\Enums\ServerState;
use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor;
use App\Enums\SuspendAction;
use App\Filament\Admin\Resources\ServerResource;
use App\Filament\Admin\Resources\ServerResource\RelationManagers\AllocationsRelationManager;
@ -51,6 +51,7 @@ use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\EditRecord;
use Filament\Support\Enums\Alignment;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Arr;
@ -135,7 +136,39 @@ class EditServer extends EditRecord
'sm' => 1,
'md' => 1,
'lg' => 1,
]),
])
->hintAction(
Action::make('view_install_log')
->label(trans('admin/server.view_install_log'))
//->visible(fn (Server $server) => $server->isFailedInstall())
->modalHeading('')
->modalSubmitAction(false)
->modalFooterActionsAlignment(Alignment::Right)
->modalCancelActionLabel(trans('filament::components/modal.actions.close.label'))
->form([
MonacoEditor::make('logs')
->hiddenLabel()
->placeholderText(trans('admin/server.no_log'))
->formatStateUsing(function (Server $server, DaemonServerRepository $serverRepository) {
try {
return $serverRepository->setServer($server)->getInstallLogs();
} catch (ConnectionException) {
Notification::make()
->title(trans('admin/server.notifications.error_connecting', ['node' => $server->node->name]))
->body(trans('admin/server.notifications.log_failed'))
->color('warning')
->warning()
->send();
} catch (Exception) {
return '';
}
return '';
})
->language('shell')
->view('filament.plugins.monaco-editor-logs'),
])
),
Textarea::make('description')
->label(trans('admin/server.description'))
@ -800,12 +833,12 @@ class EditServer extends EditRecord
Action::make('toggleInstall')
->label(trans('admin/server.toggle_install'))
->disabled(fn (Server $server) => $server->isSuspended())
->modal(fn (Server $server) => $server->status === ServerState::InstallFailed)
->modal(fn (Server $server) => $server->isFailedInstall())
->modalHeading(trans('admin/server.toggle_install_failed_header'))
->modalDescription(trans('admin/server.toggle_install_failed_desc'))
->modalSubmitActionLabel(trans('admin/server.reinstall'))
->action(function (ToggleInstallService $toggleService, ReinstallServerService $reinstallService, Server $server) {
if ($server->status === ServerState::InstallFailed) {
if ($server->isFailedInstall()) {
try {
$reinstallService->handle($server);

View File

@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Http;

View File

@ -231,7 +231,12 @@ class Server extends Model implements Validatable
public function isInstalled(): bool
{
return $this->status !== ServerState::Installing && $this->status !== ServerState::InstallFailed;
return $this->status !== ServerState::Installing && !$this->isFailedInstall();
}
public function isFailedInstall(): bool
{
return $this->status === ServerState::InstallFailed || $this->status === ServerState::ReinstallFailed;
}
public function isSuspended(): bool

View File

@ -141,4 +141,12 @@ class DaemonServerRepository extends DaemonRepository
'jtis' => [md5($id . $this->server->uuid)],
]);
}
public function getInstallLogs(): string
{
return $this->getHttpClient()
->get("/api/servers/{$this->server->uuid}/install-logs")
->throw()
->json('data');
}
}

View File

@ -9,7 +9,7 @@ class ToggleInstallService
{
public function handle(Server $server): void
{
if ($server->status === ServerState::InstallFailed) {
if ($server->isFailedInstall()) {
abort(500, trans('exceptions.server.marked_as_failed'));
}

View File

@ -64,6 +64,7 @@ return [
'reinstall_modal_heading' => 'Are you sure you want to reinstall this server?',
'reinstall_modal_description' => '!! This can result in unrecoverable data loss !!',
'server_status' => 'Server Status',
'view_install_log' => 'View install log',
'uuid' => 'UUID',
'node' => 'Node',
'short_uuid' => 'Short UUID',
@ -100,6 +101,7 @@ return [
'create_allocation' => 'Create Allocation',
'add_allocation' => 'Add Allocation',
'view' => 'View',
'no_log' => 'No Log Available',
'tabs' => [
'information' => 'Information',
'egg_configuration' => 'Egg Configuration',
@ -129,5 +131,6 @@ return [
'install_toggle_failed' => 'Could not toggle install status',
'reinstall_started' => 'Reinstall started',
'reinstall_failed' => 'Could not start reinstall',
'log_failed' => 'Could not connect to Wings to retrieve server install log.',
],
];

View File

@ -0,0 +1,184 @@
@script
<script>
$wire.on('setContent', ({ content }) => {
document.getElementById('{{ $getId() }}').editor.getModel().setValue(content);
});
$wire.on('setLanguage', ({ lang }) => {
monaco.editor.setModelLanguage(document.getElementById('{{ $getId() }}').editor.getModel(), lang);
});
</script>
@endscript
<x-dynamic-component :component="$getFieldWrapperView()" :field="$field" class="overflow-hidden">
<div x-data="{
monacoContent: $wire.$entangle('{{ $getStatePath() }}'),
previewContent: '',
fullScreenModeEnabled: false,
showPreview: false,
monacoLanguage: '{{ $getLanguage() }}',
monacoPlaceholder: {{ (int) $getShowPlaceholder() }},
monacoPlaceholderText: '{{ $getPlaceholderText() }}',
monacoLoader: {{ (int) $getShowLoader() }},
monacoFontSize: '{{ $getFontSize() }}',
lineNumbersMinChars: {{ $getLineNumbersMinChars() }},
automaticLayout: {{ (int) $getAutomaticLayout() }},
monacoId: '{{ $getId() }}',
toggleFullScreenMode() {
this.fullScreenModeEnabled = !this.fullScreenModeEnabled;
this.fullScreenModeEnabled ? document.body.classList.add('overflow-hidden')
: document.body.classList.remove('overflow-hidden');
$el.style.width = this.fullScreenModeEnabled ? '100vw'
: $el.parentElement.clientWidth + 'px';
},
monacoEditor(editor){
editor.onDidChangeModelContent((e) => {
this.monacoContent = editor.getValue();
this.updatePlaceholder(editor.getValue());
});
editor.onDidBlurEditorWidget(() => {
this.updatePlaceholder(editor.getValue());
});
editor.onDidFocusEditorWidget(() => {
this.updatePlaceholder(editor.getValue());
});
},
updatePlaceholder: function(value) {
if (value == '') {
this.monacoPlaceholder = true;
return;
}
this.monacoPlaceholder = false;
},
monacoEditorFocus(){
document.getElementById(this.monacoId).dispatchEvent(
new CustomEvent('monaco-editor-focused', { monacoId: this.monacoId })
);
},
monacoEditorAddLoaderScriptToHead() {
script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs/loader.min.js';
document.head.appendChild(script);
},
wrapPreview(value){
return `<head>{{ $getPreviewHeadEndContent() }}</head>` +
`&lt;body {{ $getPreviewBodyAttributes() }}&gt;` +
`{{ $getPreviewBodyStartContent() }}` +
`${value}` +
`{{ $getPreviewBodyEndContent() }}` +
`&lt;/body&gt;`;
},
}" x-init="
previewContent = wrapPreview(monacoContent);
$el.style.height = '500px';
$watch('fullScreenModeEnabled', value => {
if (value) {
$el.style.height = '100vh';
} else {
$el.style.height = '500px';
}
});
if(typeof _amdLoaderGlobal == 'undefined'){
monacoEditorAddLoaderScriptToHead();
}
monacoLoaderInterval = setInterval(() => {
if(typeof _amdLoaderGlobal !== 'undefined'){
// Based on https://jsfiddle.net/developit/bwgkr6uq/ which works without needing service worker. Provided by loader.min.js.
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs' }});
let proxy = URL.createObjectURL(new Blob([` self.MonacoEnvironment = { baseUrl: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min' }; importScripts('https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.49.0/min/vs/base/worker/workerMain.min.js');`], { type: 'text/javascript' }));
window.MonacoEnvironment = { getWorkerUrl: () => proxy };
require(['vs/editor/editor.main'], () => {
monaco.editor.defineTheme('custom', {{ $editorTheme() }});
document.getElementById(monacoId).editor = monaco.editor.create($refs.monacoEditorElement, {
value: monacoContent,
theme: localStorage.getItem('theme') === 'light' ? 'iPlastic' : 'custom',
fontSize: monacoFontSize,
lineNumbersMinChars: lineNumbersMinChars,
automaticLayout: automaticLayout,
language: monacoLanguage,
scrollbar: {
horizontal: 'auto',
horizontalScrollbarSize: 15,
vertical: 'auto',
verticalScrollbarSize: 15
},
wordWrap: 'on',
WrappingIndent: 'same',
readOnly: true,
minimap: {
enabled: false
}
});
$el.style.zIndex = '1';
monacoEditor(document.getElementById(monacoId).editor);
document.getElementById(monacoId).addEventListener('monaco-editor-focused', (event) => {
document.getElementById(monacoId).editor.focus();
});
updatePlaceholder(document.getElementById(monacoId).editor.getValue());
});
clearInterval(monacoLoaderInterval);
monacoLoader = false;
}
}, 5); " :id="monacoId"
class="fme-wrapper"
:class="{ 'fme-full-screen': fullScreenModeEnabled }" x-cloak>
<div class="flex items-center ml-auto">
@if($getShowFullScreenToggle())
<button type="button" aria-label="{{ __("full_screen_btn_label") }}" class="fme-full-screen-btn"
@click="toggleFullScreenMode()">
<svg class="fme-full-screen-btn-icon" x-show="!fullScreenModeEnabled"
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M16 4l4 0l0 4" />
<path d="M14 10l6 -6" />
<path d="M8 20l-4 0l0 -4" />
<path d="M4 20l6 -6" />
<path d="M16 20l4 0l0 -4" />
<path d="M14 14l6 6" />
<path d="M8 4l-4 0l0 4" />
<path d="M4 4l6 6" />
</svg>
<svg class="fme-full-screen-btn-icon" x-show="fullScreenModeEnabled"
xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 9l4 0l0 -4" />
<path d="M3 3l6 6" />
<path d="M5 15l4 0l0 4" />
<path d="M3 21l6 -6" />
<path d="M19 9l-4 0l0 -4" />
<path d="M15 9l6 -6" />
<path d="M19 15l-4 0l0 4" />
<path d="M15 15l6 6" />
</svg>
</button>
@endif
</div>
<div class="fme-container" x-show="!showPreview">
<!-- Editor -->
<div x-show="!monacoLoader" class="fme-element-wrapper">
<div x-ref="monacoEditorElement" class="fme-element" wire:ignore style="height: 100%"></div>
<div x-ref="monacoPlaceholderElement" x-show="monacoPlaceholder" @click="monacoEditorFocus()"
:style="'font-size: ' + monacoFontSize" class="fme-placeholder"
x-text="monacoPlaceholderText"></div>
</div>
</div>
</x-dynamic-component>