feat(r5): wire-elements/modal confirmations for destructive actions
Add a generic ConfirmAction modal (LivewireUI\Modal\ModalComponent). On confirm it persists exactly one AuditEvent and re-dispatches a page event so the origin applies its own state change — domain-agnostic and reused everywhere (R5). Wired the destructive actions, each writing an audit row: - Services: start/stop/restart -> confirm + audit; service state reflects result - Files: delete -> confirm + audit; entry removed - Server-Details: revoke SSH key (new per-row trash button) -> confirm + audit (carries server_id) Supporting changes: - Publish + restyle the modal container for the dark theme (void backdrop + surface panel + shadow-pop instead of the package's gray/white defaults) - Toaster island in the app layout that catches the `notify` browser event - Add alert/power/rotate/trash icons to x-icon Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
dc432e7335
commit
218806727c
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Livewire\Files;
|
namespace App\Livewire\Files;
|
||||||
|
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
|
|
@ -52,6 +53,39 @@ class Index extends Component
|
||||||
return $crumbs;
|
return $crumbs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a file (R5): opens the confirm modal, which writes the AuditEvent
|
||||||
|
* and re-dispatches `fileConfirmed`. SFTP unlink is wired in behind this later.
|
||||||
|
*/
|
||||||
|
public function confirmDelete(string $name): void
|
||||||
|
{
|
||||||
|
$this->dispatch('openModal',
|
||||||
|
component: 'modals.confirm-action',
|
||||||
|
arguments: [
|
||||||
|
'heading' => 'Datei löschen',
|
||||||
|
'body' => "„{$name}“ wird unwiderruflich aus {$this->path} entfernt.",
|
||||||
|
'confirmLabel' => 'Löschen',
|
||||||
|
'danger' => true,
|
||||||
|
'icon' => 'trash',
|
||||||
|
'auditAction' => 'file.delete',
|
||||||
|
'auditTarget' => rtrim($this->path, '/')."/{$name}",
|
||||||
|
'event' => 'fileConfirmed',
|
||||||
|
'params' => ['name' => $name],
|
||||||
|
'notify' => "„{$name}“ gelöscht.",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Applies the confirmed deletion (mock until SFTP unlink lands). */
|
||||||
|
#[On('fileConfirmed')]
|
||||||
|
public function deleteEntry(string $name): void
|
||||||
|
{
|
||||||
|
$this->entries = array_values(array_filter(
|
||||||
|
$this->entries,
|
||||||
|
fn (array $e): bool => $e['name'] !== $name,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.files.index');
|
return view('livewire.files.index');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Modals;
|
||||||
|
|
||||||
|
use App\Models\AuditEvent;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use LivewireUI\Modal\ModalComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic R5 confirmation dialog for destructive / stateful actions.
|
||||||
|
*
|
||||||
|
* The opener passes copy + an audit descriptor + a follow-up event. On confirm
|
||||||
|
* we persist exactly one AuditEvent and re-dispatch the opener's event so the
|
||||||
|
* originating page can apply its own state change. The modal stays
|
||||||
|
* domain-agnostic and is reused for every confirm in the app (R5).
|
||||||
|
*/
|
||||||
|
class ConfirmAction extends ModalComponent
|
||||||
|
{
|
||||||
|
public string $heading = 'Aktion bestätigen';
|
||||||
|
|
||||||
|
public string $body = '';
|
||||||
|
|
||||||
|
public string $confirmLabel = 'Bestätigen';
|
||||||
|
|
||||||
|
public bool $danger = false;
|
||||||
|
|
||||||
|
public string $icon = '';
|
||||||
|
|
||||||
|
/** Audit descriptor — one row written on confirm. */
|
||||||
|
public string $auditAction = '';
|
||||||
|
|
||||||
|
public ?string $auditTarget = null;
|
||||||
|
|
||||||
|
public ?int $serverId = null;
|
||||||
|
|
||||||
|
/** Follow-up event re-dispatched to the page after a successful confirm. */
|
||||||
|
public string $event = '';
|
||||||
|
|
||||||
|
/** @var array<string, mixed> */
|
||||||
|
public array $params = [];
|
||||||
|
|
||||||
|
public string $notify = 'Aktion ausgeführt.';
|
||||||
|
|
||||||
|
public static function modalMaxWidth(): string
|
||||||
|
{
|
||||||
|
return 'md';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function confirm(): void
|
||||||
|
{
|
||||||
|
if ($this->auditAction !== '') {
|
||||||
|
AuditEvent::create([
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'server_id' => $this->serverId,
|
||||||
|
'actor' => Auth::user()?->name ?? 'system',
|
||||||
|
'action' => $this->auditAction,
|
||||||
|
'target' => $this->auditTarget,
|
||||||
|
'ip' => request()->ip(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->event !== '') {
|
||||||
|
$this->dispatch($this->event, ...$this->params);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dispatch('notify', message: $this->notify);
|
||||||
|
|
||||||
|
$this->closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.modals.confirm-action');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Livewire\Servers;
|
||||||
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
|
|
@ -57,6 +58,40 @@ class Show extends Component
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke an SSH key (R5): opens the confirm modal, which writes the
|
||||||
|
* AuditEvent and re-dispatches `keyRemoved`. SSH layer removal lands later.
|
||||||
|
*/
|
||||||
|
public function confirmKeyRemoval(string $fingerprint, string $comment): void
|
||||||
|
{
|
||||||
|
$this->dispatch('openModal',
|
||||||
|
component: 'modals.confirm-action',
|
||||||
|
arguments: [
|
||||||
|
'heading' => 'SSH-Schlüssel entfernen',
|
||||||
|
'body' => "Der Schlüssel „{$comment}“ verliert den Zugang zu {$this->server->name}.",
|
||||||
|
'confirmLabel' => 'Entfernen',
|
||||||
|
'danger' => true,
|
||||||
|
'icon' => 'trash',
|
||||||
|
'auditAction' => 'ssh_key.remove',
|
||||||
|
'auditTarget' => "{$comment} · {$this->server->name}",
|
||||||
|
'serverId' => $this->server->id,
|
||||||
|
'event' => 'keyRemoved',
|
||||||
|
'params' => ['fingerprint' => $fingerprint],
|
||||||
|
'notify' => "Schlüssel „{$comment}“ entfernt.",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Applies the confirmed key removal (mock until SSH layer lands). */
|
||||||
|
#[On('keyRemoved')]
|
||||||
|
public function removeKey(string $fingerprint): void
|
||||||
|
{
|
||||||
|
$this->sshKeys = array_values(array_filter(
|
||||||
|
$this->sshKeys,
|
||||||
|
fn (array $k): bool => $k['fingerprint'] !== $fingerprint,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.servers.show');
|
return view('livewire.servers.show');
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Livewire\Services;
|
namespace App\Livewire\Services;
|
||||||
|
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
|
|
@ -63,22 +64,44 @@ class Index extends Component
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service control. Wired to the SSH layer (systemctl start/stop/restart)
|
* Service control (R5): opens the wire-elements/modal confirm dialog. The
|
||||||
* behind a confirmation modal later — see R5 markers in the view.
|
* modal writes the AuditEvent and re-dispatches `serviceConfirmed`, which
|
||||||
|
* applyService() handles. SSH exec (systemctl) is wired in behind this later.
|
||||||
*/
|
*/
|
||||||
public function start(string $name): void
|
public function confirm(string $op, string $name): void
|
||||||
{
|
{
|
||||||
// R5: routed through wire-elements/modal + SSH exec later.
|
[$heading, $phrase, $label, $action, $icon] = match ($op) {
|
||||||
|
'start' => ['Dienst starten', 'wird gestartet', 'Starten', 'service.start', 'power'],
|
||||||
|
'stop' => ['Dienst stoppen', 'wird gestoppt', 'Stoppen', 'service.stop', 'power'],
|
||||||
|
default => ['Dienst neu starten', 'wird neu gestartet', 'Neu starten', 'service.restart', 'rotate'],
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->dispatch('openModal',
|
||||||
|
component: 'modals.confirm-action',
|
||||||
|
arguments: [
|
||||||
|
'heading' => $heading,
|
||||||
|
'body' => "{$name} {$phrase}. Ausgeführt über systemctl auf {$this->server}.",
|
||||||
|
'confirmLabel' => $label,
|
||||||
|
'danger' => $op === 'stop',
|
||||||
|
'icon' => $icon,
|
||||||
|
'auditAction' => $action,
|
||||||
|
'auditTarget' => "{$name} · {$this->server}",
|
||||||
|
'event' => 'serviceConfirmed',
|
||||||
|
'params' => ['op' => $op, 'name' => $name],
|
||||||
|
'notify' => "{$name}: {$label} ausgeführt.",
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stop(string $name): void
|
/** Applies the confirmed service state change (mock until SSH exec lands). */
|
||||||
|
#[On('serviceConfirmed')]
|
||||||
|
public function applyService(string $op, string $name): void
|
||||||
{
|
{
|
||||||
// R5: routed through wire-elements/modal + SSH exec later.
|
foreach ($this->services as $i => $svc) {
|
||||||
}
|
if ($svc['name'] === $name) {
|
||||||
|
$this->services[$i]['status'] = $op === 'stop' ? 'offline' : 'online';
|
||||||
public function restart(string $name): void
|
}
|
||||||
{
|
}
|
||||||
// R5: routed through wire-elements/modal + SSH exec later.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int, array{name:string,status:string,desc:string,enabled:bool}> */
|
/** @return array<int, array{name:string,status:string,desc:string,enabled:bool}> */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Include CSS
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The modal uses TailwindCSS, if you don't use TailwindCSS you will need
|
||||||
|
| to set this parameter to true. This includes the modern-normalize css.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'include_css' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Include JS
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Livewire UI will inject the required Javascript in your blade template.
|
||||||
|
| If you want to bundle the required Javascript you can set this to false
|
||||||
|
| and add `require('vendor/wire-elements/modal/resources/js/modal');`
|
||||||
|
| to your script bundler like webpack.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
'include_js' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Modal Component Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure the default properties for a modal component.
|
||||||
|
|
|
||||||
|
| Supported modal_max_width
|
||||||
|
| 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl'
|
||||||
|
*/
|
||||||
|
'component_defaults' => [
|
||||||
|
'modal_max_width' => '2xl',
|
||||||
|
|
||||||
|
'close_modal_on_click_away' => true,
|
||||||
|
|
||||||
|
'close_modal_on_escape' => true,
|
||||||
|
|
||||||
|
'close_modal_on_escape_is_forceful' => true,
|
||||||
|
|
||||||
|
'dispatch_close_event' => false,
|
||||||
|
|
||||||
|
'destroy_on_close' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
@ -16,6 +16,10 @@
|
||||||
'shield' => '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
|
'shield' => '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
|
||||||
'plus' => '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
'plus' => '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
||||||
'logout' => '<path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>',
|
'logout' => '<path d="m16 17 5-5-5-5"/><path d="M21 12H9"/><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>',
|
||||||
|
'alert' => '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
||||||
|
'power' => '<path d="M12 2v10"/><path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>',
|
||||||
|
'rotate' => '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>',
|
||||||
|
'trash' => '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M10 11v6"/><path d="M14 11v6"/>',
|
||||||
];
|
];
|
||||||
@endphp
|
@endphp
|
||||||
<svg {{ $attributes->merge(['class' => $class]) }} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
<svg {{ $attributes->merge(['class' => $class]) }} viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,30 @@
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{-- Toaster: catches Livewire-dispatched `notify` browser events --}}
|
||||||
|
<div
|
||||||
|
x-data="{ toasts: [] }"
|
||||||
|
x-on:notify.window="
|
||||||
|
const msg = $event.detail?.message ?? $event.detail?.[0]?.message ?? 'Erledigt';
|
||||||
|
const id = (window.crypto?.randomUUID?.() ?? String(Date.now() + Math.random()));
|
||||||
|
toasts.push({ id, msg });
|
||||||
|
setTimeout(() => { toasts = toasts.filter(t => t.id !== id) }, 4000);
|
||||||
|
"
|
||||||
|
class="pointer-events-none fixed inset-x-0 bottom-4 z-50 flex flex-col items-center gap-2 px-4 sm:items-end sm:px-6"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<template x-for="t in toasts" :key="t.id">
|
||||||
|
<div
|
||||||
|
x-transition.opacity.duration.200ms
|
||||||
|
class="pointer-events-auto flex items-center gap-2.5 rounded-md border border-line bg-raised px-4 py-2.5 shadow-pop"
|
||||||
|
>
|
||||||
|
<span class="h-1.5 w-1.5 shrink-0 rounded-full bg-online"></span>
|
||||||
|
<span class="font-mono text-xs text-ink-2" x-text="t.msg"></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<livewire:wire-elements-modal />
|
||||||
@livewireScripts
|
@livewireScripts
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -118,8 +118,8 @@
|
||||||
<button type="button" class="inline-flex min-h-11 items-center rounded-sm px-2.5 py-1 font-mono text-[11px] text-ink-2 hover:bg-line hover:text-ink">
|
<button type="button" class="inline-flex min-h-11 items-center rounded-sm px-2.5 py-1 font-mono text-[11px] text-ink-2 hover:bg-line hover:text-ink">
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
</button>
|
</button>
|
||||||
{{-- R5: wire to wire-elements/modal --}}
|
{{-- R5: destructive → wire-elements/modal confirm + audit --}}
|
||||||
<button type="button" class="inline-flex min-h-11 items-center rounded-sm px-2.5 py-1 font-mono text-[11px] text-offline hover:bg-offline/10">
|
<button type="button" wire:click="confirmDelete('{{ $e['name'] }}')" class="inline-flex min-h-11 items-center rounded-sm px-2.5 py-1 font-mono text-[11px] text-offline hover:bg-offline/10">
|
||||||
Löschen
|
Löschen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<div class="p-5 sm:p-6">
|
||||||
|
<div class="flex items-start gap-3.5">
|
||||||
|
<span @class([
|
||||||
|
'grid h-10 w-10 shrink-0 place-items-center rounded-md border',
|
||||||
|
'border-offline/30 bg-offline/10 text-offline' => $danger,
|
||||||
|
'border-accent/30 bg-accent/10 text-accent-text' => ! $danger,
|
||||||
|
])>
|
||||||
|
<x-icon :name="$icon ?: ($danger ? 'alert' : 'activity')" class="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<h2 class="font-display text-base font-semibold text-ink">{{ $heading }}</h2>
|
||||||
|
<p class="mt-1 text-sm leading-relaxed text-ink-2">{{ $body }}</p>
|
||||||
|
@if ($auditTarget)
|
||||||
|
<p class="mt-2 truncate font-mono text-[11px] text-ink-4">{{ $auditTarget }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 flex items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
wire:click="$dispatch('closeModal')"
|
||||||
|
class="inline-flex min-h-11 items-center rounded-md border border-line bg-inset px-4 text-sm text-ink-2 transition-colors hover:bg-raised hover:text-ink"
|
||||||
|
>Abbrechen</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
wire:click="confirm"
|
||||||
|
wire:loading.attr="disabled"
|
||||||
|
@class([
|
||||||
|
'inline-flex min-h-11 items-center gap-2 rounded-md px-4 font-display text-sm font-semibold uppercase tracking-wide transition-colors disabled:opacity-50',
|
||||||
|
'bg-offline text-void hover:bg-offline/90' => $danger,
|
||||||
|
'bg-accent text-void hover:bg-accent-bright' => ! $danger,
|
||||||
|
])
|
||||||
|
>
|
||||||
|
<x-icon :name="$icon ?: ($danger ? 'alert' : 'activity')" class="h-4 w-4" />
|
||||||
|
{{ $confirmLabel }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -161,6 +161,13 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="truncate font-mono text-[11px] text-ink-3">{{ $key['fingerprint'] }}</p>
|
<p class="truncate font-mono text-[11px] text-ink-3">{{ $key['fingerprint'] }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{{-- R5: destructive → wire-elements/modal confirm + audit --}}
|
||||||
|
<button type="button"
|
||||||
|
wire:click="confirmKeyRemoval('{{ $key['fingerprint'] }}', '{{ $key['comment'] }}')"
|
||||||
|
class="inline-flex min-h-11 shrink-0 items-center rounded-sm px-2 text-ink-3 transition-colors hover:bg-offline/10 hover:text-offline"
|
||||||
|
title="Schlüssel entfernen">
|
||||||
|
<x-icon name="trash" class="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -63,19 +63,19 @@
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
wire:click="start('{{ $svc['name'] }}')"
|
wire:click="confirm('start', '{{ $svc['name'] }}')"
|
||||||
@disabled($svc['status'] === 'online')
|
@disabled($svc['status'] === 'online')
|
||||||
class="inline-flex min-h-11 items-center rounded-md border border-line bg-inset px-3 text-xs text-ink-2 transition-colors hover:border-online/30 hover:bg-online/10 hover:text-online disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:bg-inset disabled:hover:text-ink-2"
|
class="inline-flex min-h-11 items-center rounded-md border border-line bg-inset px-3 text-xs text-ink-2 transition-colors hover:border-online/30 hover:bg-online/10 hover:text-online disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:bg-inset disabled:hover:text-ink-2"
|
||||||
>Start</button>
|
>Start</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
wire:click="stop('{{ $svc['name'] }}')"
|
wire:click="confirm('stop', '{{ $svc['name'] }}')"
|
||||||
@disabled($svc['status'] === 'offline')
|
@disabled($svc['status'] === 'offline')
|
||||||
class="inline-flex min-h-11 items-center rounded-md border border-line bg-inset px-3 text-xs text-ink-2 transition-colors hover:border-offline/30 hover:bg-offline/10 hover:text-offline disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:bg-inset disabled:hover:text-ink-2"
|
class="inline-flex min-h-11 items-center rounded-md border border-line bg-inset px-3 text-xs text-ink-2 transition-colors hover:border-offline/30 hover:bg-offline/10 hover:text-offline disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:border-line disabled:hover:bg-inset disabled:hover:text-ink-2"
|
||||||
>Stopp</button>
|
>Stopp</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
wire:click="restart('{{ $svc['name'] }}')"
|
wire:click="confirm('restart', '{{ $svc['name'] }}')"
|
||||||
@disabled($svc['status'] === 'offline')
|
@disabled($svc['status'] === 'offline')
|
||||||
class="inline-flex min-h-11 items-center rounded-md border border-accent/25 bg-accent/10 px-3 text-xs text-accent-text transition-colors hover:bg-accent/15 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-accent/10"
|
class="inline-flex min-h-11 items-center rounded-md border border-accent/25 bg-accent/10 px-3 text-xs text-accent-text transition-colors hover:bg-accent/15 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-accent/10"
|
||||||
>Neustart</button>
|
>Neustart</button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
<div>
|
||||||
|
@isset($jsPath)
|
||||||
|
<script>{!! file_get_contents($jsPath) !!}</script>
|
||||||
|
@endisset
|
||||||
|
@isset($cssPath)
|
||||||
|
<style>{!! file_get_contents($cssPath) !!}</style>
|
||||||
|
@endisset
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-data="LivewireUIModal()"
|
||||||
|
x-on:close.stop="setShowPropertyTo(false)"
|
||||||
|
x-on:keydown.escape.window="show && closeModalOnEscape()"
|
||||||
|
x-show="show"
|
||||||
|
class="fixed inset-0 z-10 overflow-y-auto"
|
||||||
|
style="display: none;"
|
||||||
|
>
|
||||||
|
<div class="flex items-end justify-center min-h-dvh px-4 pt-4 pb-10 text-center sm:block sm:p-0">
|
||||||
|
<div
|
||||||
|
x-show="show"
|
||||||
|
x-on:click="closeModalOnClickAway()"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
x-transition:leave="ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100"
|
||||||
|
x-transition:leave-end="opacity-0"
|
||||||
|
class="fixed inset-0 transition-all transform"
|
||||||
|
>
|
||||||
|
<div class="absolute inset-0 bg-void/80 backdrop-blur-sm"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
x-show="show && showActiveComponent"
|
||||||
|
x-transition:enter="ease-out duration-300"
|
||||||
|
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave="ease-in duration-200"
|
||||||
|
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
x-bind:class="modalWidth"
|
||||||
|
class="inline-block w-full align-bottom bg-surface border border-line rounded-lg text-left overflow-hidden shadow-pop transform transition-all sm:my-8 sm:align-middle sm:w-full"
|
||||||
|
id="modal-container"
|
||||||
|
x-trap.noscroll.inert="show && showActiveComponent"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
|
@forelse($components as $id => $component)
|
||||||
|
<div x-show.immediate="activeComponent == '{{ $id }}'" x-ref="{{ $id }}" wire:key="{{ $id }}">
|
||||||
|
@livewire($component['name'], $component['arguments'], key($id))
|
||||||
|
</div>
|
||||||
|
@empty
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Reference in New Issue