feat: implement drag-and-drop functionality for reordering firewall rules in Proxmox view
This commit is contained in:
@@ -481,6 +481,54 @@ class Firewall extends Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function reorderRules(int $fromPos, int $toPos): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Proxmox API uses PUT with moveto parameter to reorder rules
|
||||||
|
$response = $this->firewallRequest(
|
||||||
|
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $fromPos,
|
||||||
|
'put',
|
||||||
|
['moveto' => $toPos]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($response->successful()) {
|
||||||
|
$this->notify('Rule reordered successfully', 'success');
|
||||||
|
$this->loadFirewallRules();
|
||||||
|
} else {
|
||||||
|
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
|
||||||
|
$this->error = 'Failed to reorder rule: ' . $message;
|
||||||
|
$this->showErrorModal = true;
|
||||||
|
\Log::error('Failed to reorder firewall rule', [
|
||||||
|
'from' => $fromPos,
|
||||||
|
'to' => $toPos,
|
||||||
|
'response' => $response->json()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errorMessage = $e->getMessage();
|
||||||
|
if (method_exists($e, 'response') && $e->response()) {
|
||||||
|
try {
|
||||||
|
$errorData = $e->response()->json();
|
||||||
|
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
|
||||||
|
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
|
||||||
|
} elseif (isset($errorData['message'])) {
|
||||||
|
$errorMessage = $errorData['message'];
|
||||||
|
}
|
||||||
|
} catch (\Exception $jsonError) {
|
||||||
|
// Response might not be JSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->error = 'Failed to reorder rule: ' . $errorMessage;
|
||||||
|
$this->showErrorModal = true;
|
||||||
|
\Log::error('Exception reordering firewall rule', [
|
||||||
|
'from' => $fromPos,
|
||||||
|
'to' => $toPos,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
'trace' => $e->getTraceAsString()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function loadAliases(): void
|
public function loadAliases(): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -92,6 +92,36 @@
|
|||||||
padding-left: 2.75rem !important;
|
padding-left: 2.75rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Drag and Drop Styles */
|
||||||
|
.rule-row {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
.rule-row.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
background-color: rgba(var(--color-primary), 0.1);
|
||||||
|
}
|
||||||
|
.rule-row.drag-over {
|
||||||
|
border-top: 3px solid rgba(var(--color-primary), 1);
|
||||||
|
}
|
||||||
|
.drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
color: rgba(var(--color-base), 0.4);
|
||||||
|
transition: color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.drag-handle:hover {
|
||||||
|
color: rgba(var(--color-primary), 1);
|
||||||
|
}
|
||||||
|
.drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
.rule-row:hover .drag-handle {
|
||||||
|
color: rgba(var(--color-base), 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
/* Firewall Log Filter Buttons */
|
/* Firewall Log Filter Buttons */
|
||||||
.firewall-filter-label {
|
.firewall-filter-label {
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
@@ -517,10 +547,79 @@
|
|||||||
|
|
||||||
<!-- Rules Table -->
|
<!-- Rules Table -->
|
||||||
@if(count($rules) > 0)
|
@if(count($rules) > 0)
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto"
|
||||||
|
x-data="{
|
||||||
|
rules: @js($rules),
|
||||||
|
draggedIndex: null,
|
||||||
|
dragOverIndex: null,
|
||||||
|
|
||||||
|
startDrag(event, index) {
|
||||||
|
this.draggedIndex = index;
|
||||||
|
event.target.closest('tr').classList.add('dragging');
|
||||||
|
event.dataTransfer.effectAllowed = 'move';
|
||||||
|
event.dataTransfer.setData('text/html', event.target.closest('tr').innerHTML);
|
||||||
|
},
|
||||||
|
|
||||||
|
endDrag(event) {
|
||||||
|
event.target.closest('tr').classList.remove('dragging');
|
||||||
|
this.draggedIndex = null;
|
||||||
|
this.dragOverIndex = null;
|
||||||
|
// Remove all drag-over classes
|
||||||
|
document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
|
||||||
|
},
|
||||||
|
|
||||||
|
dragOver(event, index) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (this.draggedIndex !== null && this.draggedIndex !== index) {
|
||||||
|
this.dragOverIndex = index;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
dragLeave(event) {
|
||||||
|
event.target.closest('tr')?.classList.remove('drag-over');
|
||||||
|
},
|
||||||
|
|
||||||
|
async drop(event, targetIndex) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.target.closest('tr')?.classList.remove('drag-over');
|
||||||
|
|
||||||
|
if (this.draggedIndex === null || this.draggedIndex === targetIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const draggedRule = this.rules[this.draggedIndex];
|
||||||
|
const targetRule = this.rules[targetIndex];
|
||||||
|
|
||||||
|
// Optimistically update UI
|
||||||
|
const movedRule = this.rules.splice(this.draggedIndex, 1)[0];
|
||||||
|
this.rules.splice(targetIndex, 0, movedRule);
|
||||||
|
|
||||||
|
// Update positions in the array
|
||||||
|
this.rules.forEach((rule, idx) => {
|
||||||
|
rule.pos = idx;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call Livewire to persist the change
|
||||||
|
try {
|
||||||
|
await @this.reorderRules(draggedRule.pos, targetRule.pos);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to reorder rules:', error);
|
||||||
|
// Reload rules on error
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getActionClass(action) {
|
||||||
|
if (action === 'ACCEPT') return 'bg-success/20 text-success';
|
||||||
|
if (action === 'REJECT') return 'bg-warning/20 text-warning';
|
||||||
|
if (action === 'DROP') return 'bg-error/20 text-error';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}">
|
||||||
<table class="w-full border-collapse">
|
<table class="w-full border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-neutral">
|
<tr class="border-b border-neutral">
|
||||||
|
<th class="text-left py-3 px-2 font-semibold text-sm w-8"></th>
|
||||||
<th class="text-left py-3 px-4 font-semibold text-sm">Position</th>
|
<th class="text-left py-3 px-4 font-semibold text-sm">Position</th>
|
||||||
<th class="text-left py-3 px-4 font-semibold text-sm">Action</th>
|
<th class="text-left py-3 px-4 font-semibold text-sm">Action</th>
|
||||||
<th class="text-left py-3 px-4 font-semibold text-sm">Source</th>
|
<th class="text-left py-3 px-4 font-semibold text-sm">Source</th>
|
||||||
@@ -532,50 +631,53 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach($rules as $rule)
|
<template x-for="(rule, index) in rules" :key="rule.pos">
|
||||||
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
|
<tr class="rule-row border-b border-neutral/20 hover:bg-background-secondary/50"
|
||||||
<td class="py-3 px-4 text-sm">{{ $rule['pos'] ?? '-' }}</td>
|
draggable="true"
|
||||||
<td class="py-3 px-4 text-sm">
|
@dragstart="startDrag($event, index)"
|
||||||
<span class="px-2 py-1 rounded text-xs font-medium
|
@dragend="endDrag($event)"
|
||||||
{{ ($rule['action'] ?? '') === 'ACCEPT' ? 'bg-success/20 text-success' : '' }}
|
@dragover="dragOver($event, index)"
|
||||||
{{ ($rule['action'] ?? '') === 'REJECT' ? 'bg-warning/20 text-warning' : '' }}
|
@dragleave="dragLeave($event)"
|
||||||
{{ ($rule['action'] ?? '') === 'DROP' ? 'bg-error/20 text-error' : '' }}">
|
@drop="drop($event, index)"
|
||||||
{{ $rule['action'] ?? '-' }}
|
:class="{ 'drag-over': dragOverIndex === index }">
|
||||||
</span>
|
<td class="py-3 px-2 text-sm">
|
||||||
|
<div class="drag-handle" title="Drag to reorder">
|
||||||
|
<x-ri-menu-line class="size-4" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4 text-sm">{{ $rule['source'] ?? '-' }}</td>
|
<td class="py-3 px-4 text-sm" x-text="rule.pos ?? '-'"></td>
|
||||||
<td class="py-3 px-4 text-sm">{{ $rule['dest'] ?? '-' }}</td>
|
|
||||||
<td class="py-3 px-4 text-sm">{{ strtoupper($rule['proto'] ?? 'any') }}</td>
|
|
||||||
<td class="py-3 px-4 text-sm">{{ $rule['dport'] ?? ($rule['sport'] ?? '-') }}</td>
|
|
||||||
<td class="py-3 px-4 text-sm">
|
<td class="py-3 px-4 text-sm">
|
||||||
@if(isset($rule['enable']) && $rule['enable'] == 0)
|
<span class="px-2 py-1 rounded text-xs font-medium"
|
||||||
<span class="px-2 py-1 rounded text-xs font-medium bg-base/20 text-base/60">Disabled</span>
|
:class="getActionClass(rule.action)"
|
||||||
@else
|
x-text="rule.action ?? '-'"></span>
|
||||||
<span class="px-2 py-1 rounded text-xs font-medium bg-success/20 text-success">Enabled</span>
|
</td>
|
||||||
@endif
|
<td class="py-3 px-4 text-sm" x-text="rule.source ?? '-'"></td>
|
||||||
|
<td class="py-3 px-4 text-sm" x-text="rule.dest ?? '-'"></td>
|
||||||
|
<td class="py-3 px-4 text-sm" x-text="(rule.proto ?? 'any').toUpperCase()"></td>
|
||||||
|
<td class="py-3 px-4 text-sm" x-text="rule.dport ?? rule.sport ?? '-'"></td>
|
||||||
|
<td class="py-3 px-4 text-sm">
|
||||||
|
<span x-show="rule.enable == 0" class="px-2 py-1 rounded text-xs font-medium bg-base/20 text-base/60">Disabled</span>
|
||||||
|
<span x-show="rule.enable != 0" class="px-2 py-1 rounded text-xs font-medium bg-success/20 text-success">Enabled</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4 text-sm">
|
<td class="py-3 px-4 text-sm">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button
|
<button
|
||||||
wire:click="toggleRule({{ $rule['pos'] ?? 0 }})"
|
@click="$wire.toggleRule(rule.pos)"
|
||||||
wire:loading.attr="disabled"
|
wire:loading.attr="disabled"
|
||||||
class="transition-colors
|
class="transition-colors"
|
||||||
{{ (isset($rule['enable']) && $rule['enable'] == 0) ? 'text-warning hover:text-warning/80' : 'text-success hover:text-success/80' }}"
|
:class="rule.enable == 0 ? 'text-warning hover:text-warning/80' : 'text-success hover:text-success/80'"
|
||||||
title="{{ (isset($rule['enable']) && $rule['enable'] == 0) ? 'Enable rule' : 'Disable rule' }}">
|
:title="rule.enable == 0 ? 'Enable rule' : 'Disable rule'">
|
||||||
@if(isset($rule['enable']) && $rule['enable'] == 0)
|
<x-ri-toggle-line x-show="rule.enable == 0" class="size-4" />
|
||||||
<x-ri-toggle-line class="size-4" />
|
<x-ri-toggle-fill x-show="rule.enable != 0" class="size-4" />
|
||||||
@else
|
|
||||||
<x-ri-toggle-fill class="size-4" />
|
|
||||||
@endif
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
wire:click="startEditRule({{ $rule['pos'] ?? 0 }})"
|
@click="$wire.startEditRule(rule.pos)"
|
||||||
class="text-primary hover:text-primary/80 transition-colors"
|
class="text-primary hover:text-primary/80 transition-colors"
|
||||||
title="Edit rule">
|
title="Edit rule">
|
||||||
<x-ri-edit-line class="size-4" />
|
<x-ri-edit-line class="size-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
wire:click="confirmDeleteRule({{ $rule['pos'] ?? 0 }})"
|
@click="$wire.confirmDeleteRule(rule.pos)"
|
||||||
class="text-error hover:text-error/80 transition-colors"
|
class="text-error hover:text-error/80 transition-colors"
|
||||||
title="Delete rule">
|
title="Delete rule">
|
||||||
<x-ri-delete-bin-line class="size-4" />
|
<x-ri-delete-bin-line class="size-4" />
|
||||||
@@ -583,7 +685,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@endforeach
|
</template>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user