feat: Complete WGRplane - Vue.js frontend with Glassmorphism UI, 2-Column Policy, Webhook Management
This commit is contained in:
@@ -6,7 +6,16 @@
|
|||||||
+ Add Peer
|
+ Add Peer
|
||||||
</GlassButton>
|
</GlassButton>
|
||||||
</div>
|
</div>
|
||||||
<table class="w-full text-left text-white/90">
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="text-center py-8 text-white/50">Loading peers...</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-else-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table v-else class="w-full text-left text-white/90">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b border-white/10">
|
<tr class="border-b border-white/10">
|
||||||
<th class="p-3 text-white/70">Peer Name</th>
|
<th class="p-3 text-white/70">Peer Name</th>
|
||||||
@@ -238,7 +247,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { TransitionRoot, TransitionChild, Dialog, DialogPanel, DialogTitle } from '@headlessui/vue'
|
import { TransitionRoot, TransitionChild, Dialog, DialogPanel, DialogTitle } from '@headlessui/vue'
|
||||||
import GlassCard from './glass/GlassCard.vue'
|
import GlassCard from './glass/GlassCard.vue'
|
||||||
import GlassInput from './glass/GlassInput.vue'
|
import GlassInput from './glass/GlassInput.vue'
|
||||||
@@ -262,32 +271,28 @@ const props = defineProps<{
|
|||||||
serverId: string
|
serverId: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const peers = ref<Peer[]>([
|
const peers = ref<Peer[]>([])
|
||||||
{
|
const loading = ref(true)
|
||||||
id: '1',
|
const error = ref<string | null>(null)
|
||||||
name: 'Client 1',
|
|
||||||
publicKey: 'xTIBA5rboUvnH3h2+JQ0qf0pI0n6KJ7g5Uc5gG0q0=',
|
const fetchPeers = async () => {
|
||||||
ip: '10.0.0.2/32',
|
loading.value = true
|
||||||
allowedAccess: ['192.168.1.0/24', '10.0.0.1/32'],
|
error.value = null
|
||||||
allowInternet: true
|
try {
|
||||||
},
|
const response = await fetch(`/api/servers/${props.serverId}/peers`)
|
||||||
{
|
if (!response.ok) throw new Error('Failed to fetch peers')
|
||||||
id: '2',
|
peers.value = await response.json()
|
||||||
name: 'Client 2',
|
} catch (err) {
|
||||||
publicKey: 'yUIBA6scpVwoH4h3+KR1qf1qJ1o7KJ8h6Vd6hH1r1=',
|
error.value = err instanceof Error ? err.message : 'An error occurred'
|
||||||
ip: '10.0.0.3/32',
|
console.error('Fetch peers error:', err)
|
||||||
allowedAccess: [],
|
} finally {
|
||||||
allowInternet: false
|
loading.value = false
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Client 3',
|
|
||||||
publicKey: 'zVJCB7tdqWxpI5h4+LS2rg2rK2p8LK9i7We7iI2s2=',
|
|
||||||
ip: '10.0.0.4/32',
|
|
||||||
allowedAccess: ['172.16.0.0/16'],
|
|
||||||
allowInternet: true
|
|
||||||
}
|
}
|
||||||
])
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchPeers()
|
||||||
|
})
|
||||||
|
|
||||||
const isAddPeerModalOpen = ref(false)
|
const isAddPeerModalOpen = ref(false)
|
||||||
const isQRCodeModalOpen = ref(false)
|
const isQRCodeModalOpen = ref(false)
|
||||||
@@ -333,13 +338,9 @@ const updatePeer = async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (!response.ok) throw new Error('Failed to update peer')
|
if (!response.ok) throw new Error('Failed to update peer')
|
||||||
const updatedPeer = await response.json()
|
|
||||||
const index = peers.value.findIndex(p => p.id === updatedPeer.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
peers.value[index] = updatedPeer
|
|
||||||
}
|
|
||||||
isEditPeerModalOpen.value = false
|
isEditPeerModalOpen.value = false
|
||||||
selectedPeerForEdit.value = null
|
selectedPeerForEdit.value = null
|
||||||
|
await fetchPeers()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update peer error:', error)
|
console.error('Update peer error:', error)
|
||||||
alert('Failed to update peer. Check console for details.')
|
alert('Failed to update peer. Check console for details.')
|
||||||
@@ -354,10 +355,9 @@ const addPeer = async () => {
|
|||||||
body: JSON.stringify(newPeer.value)
|
body: JSON.stringify(newPeer.value)
|
||||||
})
|
})
|
||||||
if (!response.ok) throw new Error('Failed to add peer')
|
if (!response.ok) throw new Error('Failed to add peer')
|
||||||
const createdPeer = await response.json()
|
|
||||||
peers.value.push({ ...createdPeer, name: `Client ${peers.value.length + 1}` })
|
|
||||||
isAddPeerModalOpen.value = false
|
isAddPeerModalOpen.value = false
|
||||||
newPeer.value = { publicKey: '', ip: '', allowedAccess: [], allowInternet: false, expiresAt: null, dataLimitGB: null }
|
newPeer.value = { publicKey: '', ip: '', allowedAccess: [], allowInternet: false, expiresAt: null, dataLimitGB: null }
|
||||||
|
await fetchPeers()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Add peer error:', error)
|
console.error('Add peer error:', error)
|
||||||
alert('Failed to add peer. Check console for details.')
|
alert('Failed to add peer. Check console for details.')
|
||||||
@@ -369,7 +369,7 @@ const deletePeer = async (id: string) => {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/peers/${id}`, { method: 'DELETE' })
|
const response = await fetch(`/api/peers/${id}`, { method: 'DELETE' })
|
||||||
if (!response.ok) throw new Error('Failed to delete peer')
|
if (!response.ok) throw new Error('Failed to delete peer')
|
||||||
peers.value = peers.value.filter(peer => peer.id !== id)
|
await fetchPeers()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete peer error:', error)
|
console.error('Delete peer error:', error)
|
||||||
alert('Failed to delete peer. Check console for details.')
|
alert('Failed to delete peer. Check console for details.')
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ ChartJS.register(
|
|||||||
Filler
|
Filler
|
||||||
)
|
)
|
||||||
|
|
||||||
const { isConnected, stats, error, connect } = useWebSocket('ws://localhost:8080/ws/stats')
|
const { isConnected, stats, error, connect } = useWebSocket('ws://localhost:10087/ws/stats')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
connect()
|
connect()
|
||||||
|
|||||||
@@ -8,21 +8,77 @@
|
|||||||
</GlassButton>
|
</GlassButton>
|
||||||
<h1 class="text-3xl font-bold text-white">Server Detail</h1>
|
<h1 class="text-3xl font-bold text-white">Server Detail</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-white/70">Server ID: {{ $route.params.id }}</p>
|
|
||||||
|
|
||||||
<GlassCard class="p-6">
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="text-center py-8 text-white/50">Loading server information...</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Information -->
|
||||||
|
<GlassCard v-if="server" class="p-6">
|
||||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">Server Information</h2>
|
<h2 class="text-xl font-semibold text-cyan-400 mb-4">Server Information</h2>
|
||||||
<p class="text-white/50">Server details will be displayed here.</p>
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-white/50 text-sm">Server Name</p>
|
||||||
|
<p class="text-white text-lg">{{ server.name }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-white/50 text-sm">Mode</p>
|
||||||
|
<span
|
||||||
|
class="inline-block px-3 py-1 rounded-full text-sm"
|
||||||
|
:class="server.mode === 'forward' ? 'bg-cyan-500/20 text-cyan-400' : 'bg-purple-500/20 text-purple-400'"
|
||||||
|
>
|
||||||
|
{{ server.mode }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-white/50 text-sm">Public Key</p>
|
||||||
|
<p class="text-white font-mono text-sm break-all">{{ server.publicKey }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-white/50 text-sm">Endpoint</p>
|
||||||
|
<p class="text-white font-mono text-sm">{{ server.endpoint || 'Not set' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
|
||||||
<GlassCard class="p-6">
|
<!-- Peer Table -->
|
||||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">Peer Table</h2>
|
<PeerTable v-if="server" :server-id="$route.params.id as string" />
|
||||||
<p class="text-white/50">Peer list for this server will be displayed here.</p>
|
|
||||||
</GlassCard>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
import GlassCard from '../components/glass/GlassCard.vue'
|
import GlassCard from '../components/glass/GlassCard.vue'
|
||||||
import GlassButton from '../components/glass/GlassButton.vue'
|
import GlassButton from '../components/glass/GlassButton.vue'
|
||||||
|
import PeerTable from '../components/PeerTable.vue'
|
||||||
|
|
||||||
|
interface Server {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
mode: 'forward' | 'standalone'
|
||||||
|
publicKey: string
|
||||||
|
endpoint?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = ref<Server | null>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const serverId = (window as any).$route?.params?.id || '1'
|
||||||
|
const response = await fetch(`/api/servers/${serverId}`)
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch server details')
|
||||||
|
server.value = await response.json()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : 'An error occurred'
|
||||||
|
console.error('Failed to load server:', err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,20 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<h1 class="text-3xl font-bold text-white">{{ t('settings') }}</h1>
|
<h1 class="text-3xl font-bold text-white">{{ t('settings') }}</h1>
|
||||||
|
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="text-center py-8 text-white/50">Loading settings...</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-else-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<!-- Success State -->
|
||||||
|
<div v-if="success" class="p-4 bg-green-500/20 border border-green-500/50 rounded-lg text-green-400">
|
||||||
|
{{ success }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<GlassCard class="p-6">
|
<GlassCard class="p-6">
|
||||||
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('dashboardSettings') }}</h2>
|
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('dashboardSettings') }}</h2>
|
||||||
<p class="text-white/50">Dashboard configuration options will be displayed here.</p>
|
<p class="text-white/50">Dashboard configuration options will be displayed here.</p>
|
||||||
@@ -114,15 +128,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4 mt-6">
|
<div class="flex gap-4 mt-6">
|
||||||
<GlassButton @click="testEmail" class="from-cyan-500 to-blue-500">{{ t('testEmail') }}</GlassButton>
|
<GlassButton @click="testEmail" class="from-cyan-500 to-blue-500" :disabled="loading">{{ t('testEmail') }}</GlassButton>
|
||||||
<GlassButton @click="saveSettings" class="from-green-500 to-teal-500">{{ t('saveSettings') }}</GlassButton>
|
<GlassButton @click="saveSettings" class="from-green-500 to-teal-500" :disabled="loading">{{ t('saveSettings') }}</GlassButton>
|
||||||
</div>
|
</div>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive } from 'vue'
|
import { reactive, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import GlassCard from '../components/glass/GlassCard.vue'
|
import GlassCard from '../components/glass/GlassCard.vue'
|
||||||
import GlassInput from '../components/glass/GlassInput.vue'
|
import GlassInput from '../components/glass/GlassInput.vue'
|
||||||
@@ -144,17 +159,64 @@ const smtpSettings = reactive({
|
|||||||
FromName: ''
|
FromName: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
const success = ref<string | null>(null)
|
||||||
|
|
||||||
|
const fetchSettings = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/smtp')
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch SMTP settings')
|
||||||
|
const data = await response.json()
|
||||||
|
Object.assign(smtpSettings, data)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fetch SMTP settings error:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSettings()
|
||||||
|
})
|
||||||
|
|
||||||
const switchLanguage = (lang: string) => {
|
const switchLanguage = (lang: string) => {
|
||||||
locale.value = lang
|
locale.value = lang
|
||||||
}
|
}
|
||||||
|
|
||||||
const testEmail = () => {
|
const testEmail = async () => {
|
||||||
console.log('Test Email with settings:', JSON.parse(JSON.stringify(smtpSettings)))
|
loading.value = true
|
||||||
alert('Test email functionality (mock) - check console for settings')
|
error.value = null
|
||||||
|
success.value = null
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/smtp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ...smtpSettings, test: true })
|
||||||
|
})
|
||||||
|
if (!response.ok) throw new Error('Failed to test email')
|
||||||
|
success.value = 'Test email sent successfully!'
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : 'Failed to test email'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveSettings = () => {
|
const saveSettings = async () => {
|
||||||
console.log('Saving SMTP settings (mock):', JSON.parse(JSON.stringify(smtpSettings)))
|
loading.value = true
|
||||||
alert('SMTP settings saved (mock)')
|
error.value = null
|
||||||
|
success.value = null
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/settings/smtp', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(smtpSettings)
|
||||||
|
})
|
||||||
|
if (!response.ok) throw new Error('Failed to save settings')
|
||||||
|
success.value = 'Settings saved successfully!'
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : 'Failed to save settings'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,7 +10,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="text-white/70">Server ID: {{ $route.params.id }}</p>
|
<p class="text-white/70">Server ID: {{ $route.params.id }}</p>
|
||||||
|
|
||||||
<GlassCard class="p-6">
|
<!-- Loading State -->
|
||||||
|
<div v-if="loading" class="text-center py-8 text-white/50">Loading webhooks...</div>
|
||||||
|
|
||||||
|
<!-- Error State -->
|
||||||
|
<div v-else-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GlassCard v-else class="p-6">
|
||||||
<WebhookList
|
<WebhookList
|
||||||
:webhooks="webhooks"
|
:webhooks="webhooks"
|
||||||
@add="showAddForm"
|
@add="showAddForm"
|
||||||
@@ -56,16 +64,40 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
import GlassCard from '../components/glass/GlassCard.vue'
|
import GlassCard from '../components/glass/GlassCard.vue'
|
||||||
import GlassButton from '../components/glass/GlassButton.vue'
|
import GlassButton from '../components/glass/GlassButton.vue'
|
||||||
import WebhookList from '../components/WebhookList.vue'
|
import WebhookList from '../components/WebhookList.vue'
|
||||||
import WebhookForm from '../components/WebhookForm.vue'
|
import WebhookForm from '../components/WebhookForm.vue'
|
||||||
import { mockWebhooks, type Webhook } from '../types/webhook'
|
import type { Webhook } from '../types/webhook'
|
||||||
|
|
||||||
const webhooks = ref<Webhook[]>([...mockWebhooks])
|
const route = useRoute()
|
||||||
|
const webhooks = ref<Webhook[]>([])
|
||||||
const showForm = ref(false)
|
const showForm = ref(false)
|
||||||
const editingWebhook = ref<Webhook | undefined>(undefined)
|
const editingWebhook = ref<Webhook | undefined>(undefined)
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
const fetchWebhooks = async () => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
const serverId = route.params.id as string
|
||||||
|
const response = await fetch(`/api/servers/${serverId}/webhooks`)
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch webhooks')
|
||||||
|
webhooks.value = await response.json()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err instanceof Error ? err.message : 'An error occurred'
|
||||||
|
console.error('Fetch webhooks error:', err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchWebhooks()
|
||||||
|
})
|
||||||
|
|
||||||
const showAddForm = () => {
|
const showAddForm = () => {
|
||||||
editingWebhook.value = undefined
|
editingWebhook.value = undefined
|
||||||
@@ -82,33 +114,41 @@ const closeForm = () => {
|
|||||||
editingWebhook.value = undefined
|
editingWebhook.value = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveWebhook = (webhookData: Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>) => {
|
const saveWebhook = async (webhookData: Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>) => {
|
||||||
|
try {
|
||||||
|
const serverId = route.params.id as string
|
||||||
if (editingWebhook.value) {
|
if (editingWebhook.value) {
|
||||||
// Update existing webhook
|
const response = await fetch(`/api/webhooks/${editingWebhook.value.id}`, {
|
||||||
const index = webhooks.value.findIndex(w => w.id === editingWebhook.value!.id)
|
method: 'PUT',
|
||||||
if (index !== -1) {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
webhooks.value[index] = {
|
body: JSON.stringify(webhookData)
|
||||||
...webhooks.value[index],
|
})
|
||||||
...webhookData,
|
if (!response.ok) throw new Error('Failed to update webhook')
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Add new webhook
|
const response = await fetch(`/api/servers/${serverId}/webhooks`, {
|
||||||
const newWebhook: Webhook = {
|
method: 'POST',
|
||||||
...webhookData,
|
headers: { 'Content-Type': 'application/json' },
|
||||||
id: Date.now().toString(),
|
body: JSON.stringify(webhookData)
|
||||||
createdAt: new Date().toISOString(),
|
})
|
||||||
updatedAt: new Date().toISOString()
|
if (!response.ok) throw new Error('Failed to create webhook')
|
||||||
}
|
|
||||||
webhooks.value.push(newWebhook)
|
|
||||||
}
|
}
|
||||||
closeForm()
|
closeForm()
|
||||||
|
await fetchWebhooks()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Save webhook error:', err)
|
||||||
|
alert('Failed to save webhook. Check console for details.')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteWebhook = (id: string) => {
|
const deleteWebhook = async (id: string) => {
|
||||||
if (confirm('Are you sure you want to delete this webhook?')) {
|
if (!confirm('Are you sure you want to delete this webhook?')) return
|
||||||
webhooks.value = webhooks.value.filter(w => w.id !== id)
|
try {
|
||||||
|
const response = await fetch(`/api/webhooks/${id}`, { method: 'DELETE' })
|
||||||
|
if (!response.ok) throw new Error('Failed to delete webhook')
|
||||||
|
await fetchWebhooks()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Delete webhook error:', err)
|
||||||
|
alert('Failed to delete webhook. Check console for details.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user