Add saved firewall configuration functionality
- Implemented the ability to save, view, apply, and delete firewall configurations for users. - Created a new model for FirewallConfiguration and corresponding database migration. - Enhanced the firewall view to include a tab for managing saved configurations.
This commit is contained in:
@@ -365,6 +365,16 @@
|
||||
</div>
|
||||
<div x-show="activeTab === 'logs'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
||||
</button>
|
||||
<button @click="activeTab = 'saved'"
|
||||
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
||||
:class="activeTab === 'saved' ? 'text-primary' : 'text-base/60 hover:text-base'">
|
||||
<div class="flex items-center gap-2">
|
||||
<x-ri-save-3-line class="size-4" />
|
||||
<span>Saved Configurations</span>
|
||||
<span class="px-2 py-0.5 bg-primary/10 text-primary rounded-full text-xs font-bold">{{ count($savedConfigurations) }}</span>
|
||||
</div>
|
||||
<div x-show="activeTab === 'saved'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Tab Contents --}}
|
||||
@@ -1384,6 +1394,87 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- SAVED CONFIGURATIONS TAB --}}
|
||||
<div x-show="activeTab === 'saved'" x-transition>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold">Saved Configurations</h3>
|
||||
<p class="text-sm text-base/60 mt-1">Save and reuse firewall configurations across your VPS instances</p>
|
||||
</div>
|
||||
<button type="button" wire:click="openSaveConfigModal" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
|
||||
<x-ri-save-3-line class="size-4" />
|
||||
Save Current Configuration
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if(count($savedConfigurations) > 0)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-neutral/20">
|
||||
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Configuration Name</th>
|
||||
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Rules</th>
|
||||
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Policies</th>
|
||||
<th class="text-left py-3 px-4 text-xs font-semibold text-base/70 uppercase">Created</th>
|
||||
<th class="text-center py-3 px-4 text-xs font-semibold text-base/70 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($savedConfigurations as $config)
|
||||
<tr class="border-b border-neutral/10 hover:bg-background-secondary/30 transition-colors">
|
||||
<td class="py-3 px-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<x-ri-shield-star-line class="size-4 text-primary" />
|
||||
</div>
|
||||
<span class="font-medium">{{ $config['configuration_name'] }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<span class="px-2 py-1 bg-info/10 text-info rounded-full text-xs font-medium">
|
||||
{{ is_array($config['firewall_rules']) ? count($config['firewall_rules']) : 0 }} rules
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="px-2 py-0.5 text-xs rounded {{ $config['incoming_policy'] === 'DROP' ? 'bg-error/10 text-error' : 'bg-success/10 text-success' }}">
|
||||
IN: {{ $config['incoming_policy'] }}
|
||||
</span>
|
||||
<span class="px-2 py-0.5 text-xs rounded {{ $config['outgoing_policy'] === 'DROP' ? 'bg-error/10 text-error' : 'bg-success/10 text-success' }}">
|
||||
OUT: {{ $config['outgoing_policy'] }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm text-base/70">
|
||||
{{ \Carbon\Carbon::parse($config['created_at'])->format('M d, Y H:i') }}
|
||||
</td>
|
||||
<td class="py-3 px-4">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<button type="button" wire:click="confirmApplyConfiguration({{ $config['id'] }})" class="p-2 text-success hover:bg-success/10 rounded-lg transition-colors" title="Apply Configuration">
|
||||
<x-ri-play-circle-line class="size-5" />
|
||||
</button>
|
||||
<button type="button" wire:click="viewConfiguration({{ $config['id'] }})" class="p-2 text-info hover:bg-info/10 rounded-lg transition-colors" title="View Details">
|
||||
<x-ri-eye-line class="size-5" />
|
||||
</button>
|
||||
<button type="button" wire:click="confirmDeleteConfiguration({{ $config['id'] }})" class="p-2 text-error hover:bg-error/10 rounded-lg transition-colors" title="Delete Configuration">
|
||||
<x-ri-delete-bin-line class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-12 text-base/60">
|
||||
<x-ri-save-3-line class="size-16 mx-auto mb-4 opacity-30" />
|
||||
<h3 class="text-lg font-semibold mb-2">No Saved Configurations</h3>
|
||||
<p class="text-sm">Save your current firewall configuration to reuse it later or apply it to other VPS instances</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@@ -1853,6 +1944,298 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Save Configuration Modal --}}
|
||||
@if($showSaveConfigModal)
|
||||
<div class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
|
||||
<button wire:click="closeSaveConfigModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
||||
<x-ri-close-line class="size-5" />
|
||||
</button>
|
||||
<div class="flex items-start gap-4 mb-6">
|
||||
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<x-ri-save-3-line class="size-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-1">Save Current Configuration</h3>
|
||||
<p class="text-sm text-base/70">Save your current firewall rules and policies as a reusable configuration</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Configuration Name</label>
|
||||
<input type="text" wire:model="newConfigName" placeholder="e.g., Web Server Rules"
|
||||
class="w-full px-4 py-2 bg-background border border-neutral rounded-lg focus:border-primary focus:outline-none transition-colors">
|
||||
@error('newConfigName')
|
||||
<p class="text-error text-xs mt-1">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="bg-background/50 border border-neutral/30 rounded-lg p-4">
|
||||
<h4 class="text-sm font-semibold mb-3">Configuration Summary</h4>
|
||||
<div class="grid grid-cols-2 gap-3 text-sm">
|
||||
<div class="flex items-center justify-between p-2 bg-background rounded">
|
||||
<span class="text-base/60">Rules</span>
|
||||
<span class="font-semibold">{{ count($rules) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-2 bg-background rounded">
|
||||
<span class="text-base/60">Incoming Policy</span>
|
||||
<span class="font-semibold {{ $policyIn === 'DROP' ? 'text-error' : 'text-success' }}">{{ $policyIn }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-2 bg-background rounded">
|
||||
<span class="text-base/60">Outgoing Policy</span>
|
||||
<span class="font-semibold {{ $policyOut === 'DROP' ? 'text-error' : 'text-success' }}">{{ $policyOut }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 justify-end mt-6 pt-4 border-t border-neutral/20">
|
||||
<button wire:click="closeSaveConfigModal" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
|
||||
Cancel
|
||||
</button>
|
||||
<button wire:click="saveConfiguration" wire:loading.attr="disabled" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
||||
<span wire:loading.remove wire:target="saveConfiguration">Save Configuration</span>
|
||||
<span wire:loading wire:target="saveConfiguration">Saving...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- View Configuration Modal --}}
|
||||
@if($showViewConfigModal && $viewingConfiguration)
|
||||
<div class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<div class="p-6 border-b border-neutral/20">
|
||||
<button wire:click="closeViewConfigModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
||||
<x-ri-close-line class="size-5" />
|
||||
</button>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-12 h-12 rounded-full bg-info/10 flex items-center justify-center flex-shrink-0">
|
||||
<x-ri-eye-line class="size-6 text-info" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-1">{{ $viewingConfiguration->configuration_name }}</h3>
|
||||
<p class="text-sm text-base/70">Created on {{ $viewingConfiguration->created_at->format('M d, Y H:i') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 overflow-y-auto flex-1">
|
||||
{{-- Policies --}}
|
||||
<div class="mb-6">
|
||||
<h4 class="text-sm font-semibold mb-3">Traffic Policies</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="p-4 bg-background rounded-lg border border-neutral/20">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-base/60">Incoming Traffic</span>
|
||||
<span class="px-3 py-1 rounded-full text-sm font-semibold {{ $viewingConfiguration->incoming_policy === 'DROP' ? 'bg-error/10 text-error' : 'bg-success/10 text-success' }}">
|
||||
{{ $viewingConfiguration->incoming_policy }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-background rounded-lg border border-neutral/20">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-base/60">Outgoing Traffic</span>
|
||||
<span class="px-3 py-1 rounded-full text-sm font-semibold {{ $viewingConfiguration->outgoing_policy === 'DROP' ? 'bg-error/10 text-error' : 'bg-success/10 text-success' }}">
|
||||
{{ $viewingConfiguration->outgoing_policy }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Compatibility Warnings --}}
|
||||
@if(count($configCompatibilityIssues) > 0)
|
||||
<div class="mb-6 p-4 bg-warning/10 border border-warning/30 rounded-lg">
|
||||
<div class="flex items-start gap-3">
|
||||
<x-ri-error-warning-line class="size-5 text-warning flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-warning mb-2">Compatibility Issues</h4>
|
||||
<p class="text-xs text-warning/80 mb-2">The following rules may not be compatible with this VPS:</p>
|
||||
<ul class="text-xs text-warning/80 space-y-1">
|
||||
@foreach($configCompatibilityIssues as $issue)
|
||||
<li>• Rule #{{ $issue['rule_index'] + 1 }}: {{ implode(', ', $issue['issues']) }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Rules --}}
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold mb-3">Firewall Rules ({{ count($viewingConfiguration->firewall_rules) }})</h4>
|
||||
@if(count($viewingConfiguration->firewall_rules) > 0)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-neutral/20">
|
||||
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">#</th>
|
||||
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Action</th>
|
||||
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Direction</th>
|
||||
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Source</th>
|
||||
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Destination</th>
|
||||
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Protocol</th>
|
||||
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Port</th>
|
||||
<th class="text-left py-2 px-3 text-xs font-semibold text-base/70 uppercase">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($viewingConfiguration->firewall_rules as $index => $rule)
|
||||
@php
|
||||
$hasIssue = collect($configCompatibilityIssues)->contains('rule_index', $index);
|
||||
@endphp
|
||||
<tr class="border-b border-neutral/10 {{ $hasIssue ? 'bg-warning/5' : '' }}">
|
||||
<td class="py-2 px-3 text-base/60">{{ $index + 1 }}</td>
|
||||
<td class="py-2 px-3">
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium {{ ($rule['action'] ?? 'ACCEPT') === 'ACCEPT' ? 'bg-success/10 text-success' : 'bg-error/10 text-error' }}">
|
||||
{{ $rule['action'] ?? 'ACCEPT' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 px-3">{{ strtoupper($rule['type'] ?? '-') }}</td>
|
||||
<td class="py-2 px-3 font-mono text-xs">{{ $rule['source'] ?? 'any' }}</td>
|
||||
<td class="py-2 px-3 font-mono text-xs">{{ $rule['dest'] ?? 'any' }}</td>
|
||||
<td class="py-2 px-3">{{ $rule['proto'] ?? '-' }}</td>
|
||||
<td class="py-2 px-3 font-mono text-xs">{{ $rule['dport'] ?? '-' }}</td>
|
||||
<td class="py-2 px-3">
|
||||
@if($hasIssue)
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium bg-warning/10 text-warning">Incompatible</span>
|
||||
@elseif(isset($rule['enable']) && $rule['enable'] == 0)
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium bg-base/10 text-base/60">Disabled</span>
|
||||
@else
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium bg-success/10 text-success">Enabled</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-sm text-base/60 text-center py-4">No rules in this configuration</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-neutral/20 bg-background/50">
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button wire:click="closeViewConfigModal" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
|
||||
Close
|
||||
</button>
|
||||
<button wire:click="confirmApplyConfiguration({{ $viewingConfiguration->id }})" class="px-4 py-2 bg-success text-white rounded-lg hover:bg-success/90 transition-colors text-sm font-medium flex items-center gap-2">
|
||||
<x-ri-play-circle-line class="size-4" />
|
||||
Apply Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Apply Configuration Modal --}}
|
||||
@if($showApplyConfigModal && $applyingConfiguration)
|
||||
<div class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="relative w-full max-w-lg bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
|
||||
<button wire:click="closeApplyConfigModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
||||
<x-ri-close-line class="size-5" />
|
||||
</button>
|
||||
<div class="flex items-start gap-4 mb-6">
|
||||
<div class="w-12 h-12 rounded-full bg-success/10 flex items-center justify-center flex-shrink-0">
|
||||
<x-ri-play-circle-line class="size-6 text-success" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-1">Apply Configuration</h3>
|
||||
<p class="text-sm text-base/70">Apply "{{ $applyingConfiguration->configuration_name }}" to this VPS</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-warning/10 border border-warning/30 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<x-ri-error-warning-line class="size-5 text-warning flex-shrink-0 mt-0.5" />
|
||||
<div class="text-sm">
|
||||
<p class="font-semibold text-warning mb-1">Warning</p>
|
||||
<p class="text-warning/80">This will replace all existing firewall rules with the rules from this configuration. This action cannot be undone.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(count($applyCompatibilityWarnings) > 0)
|
||||
<div class="bg-info/10 border border-info/30 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<x-ri-information-line class="size-5 text-info flex-shrink-0 mt-0.5" />
|
||||
<div class="text-sm">
|
||||
<p class="font-semibold text-info mb-1">Compatibility Notice</p>
|
||||
<p class="text-info/80">{{ count($applyCompatibilityWarnings) }} rule(s) in this configuration are not fully compatible with this VPS and will be disabled.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-background/50 border border-neutral/30 rounded-lg p-4 mb-6">
|
||||
<h4 class="text-sm font-semibold mb-3">Configuration Details</h4>
|
||||
<div class="grid grid-cols-3 gap-3 text-sm">
|
||||
<div class="text-center p-2 bg-background rounded">
|
||||
<p class="text-xs text-base/60 mb-1">Rules</p>
|
||||
<p class="font-semibold">{{ count($applyingConfiguration->firewall_rules) }}</p>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-background rounded">
|
||||
<p class="text-xs text-base/60 mb-1">IN Policy</p>
|
||||
<p class="font-semibold {{ $applyingConfiguration->incoming_policy === 'DROP' ? 'text-error' : 'text-success' }}">{{ $applyingConfiguration->incoming_policy }}</p>
|
||||
</div>
|
||||
<div class="text-center p-2 bg-background rounded">
|
||||
<p class="text-xs text-base/60 mb-1">OUT Policy</p>
|
||||
<p class="font-semibold {{ $applyingConfiguration->outgoing_policy === 'DROP' ? 'text-error' : 'text-success' }}">{{ $applyingConfiguration->outgoing_policy }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button wire:click="closeApplyConfigModal" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
|
||||
Cancel
|
||||
</button>
|
||||
<button wire:click="applyConfiguration" wire:loading.attr="disabled" class="px-4 py-2 bg-success text-white rounded-lg hover:bg-success/90 transition-colors text-sm font-medium">
|
||||
<span wire:loading.remove wire:target="applyConfiguration">Apply Configuration</span>
|
||||
<span wire:loading wire:target="applyConfiguration">Applying...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Delete Configuration Modal --}}
|
||||
@if($showDeleteConfigModal && $deletingConfigId)
|
||||
<div class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div class="relative w-full max-w-md bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl p-6">
|
||||
<button wire:click="cancelDeleteConfiguration" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
||||
<x-ri-close-line class="size-5" />
|
||||
</button>
|
||||
<div class="flex items-start gap-4 mb-4">
|
||||
<div class="w-12 h-12 rounded-full bg-error/10 flex items-center justify-center flex-shrink-0">
|
||||
<x-ri-delete-bin-line class="size-6 text-error" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-2">Delete Configuration</h3>
|
||||
<p class="text-sm text-base/70">Are you sure you want to delete this saved configuration? This action cannot be undone.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button wire:click="cancelDeleteConfiguration" class="px-4 py-2 bg-background border border-neutral rounded-lg hover:bg-background-secondary transition-colors text-sm font-medium">
|
||||
Cancel
|
||||
</button>
|
||||
<button wire:click="deleteConfiguration" wire:loading.attr="disabled"
|
||||
style="padding: 0.5rem 1rem; background-color: #ef4444; color: white; border-radius: 0.5rem; transition: all 0.2s; font-size: 0.875rem; font-weight: 500;"
|
||||
onmouseover="this.style.backgroundColor='#dc2626'"
|
||||
onmouseout="this.style.backgroundColor='#ef4444'">
|
||||
<span wire:loading.remove wire:target="deleteConfiguration">Delete</span>
|
||||
<span wire:loading wire:target="deleteConfiguration">Deleting...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@script
|
||||
<script>
|
||||
// IP location helper functions
|
||||
|
||||
Reference in New Issue
Block a user