Private
Public Access
1
0

feat: implement drag-and-drop functionality for reordering firewall rules in Proxmox view

This commit is contained in:
2025-12-04 11:25:35 -05:00
parent 33144e96ae
commit 7b1bfe8b23
2 changed files with 182 additions and 32 deletions

View File

@@ -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 {

View File

@@ -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>