feat(wireguard): Quick-Toggles für Interfaces + Peers

Inline Switch statt read-only StatusDot — konsistent mit allen anderen Seiten.
Server-Interfaces, Client-Interfaces und Peers im Peer-Drawer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-24 11:06:19 +02:00
parent ee192a1c9c
commit b93f4e6361
2 changed files with 60 additions and 4 deletions

View File

@@ -12,7 +12,6 @@ import apiClient, { isEnvelope } from '../../api/client'
import DataTable from '../../components/DataTable' import DataTable from '../../components/DataTable'
import EmptyState from '../../components/EmptyState' import EmptyState from '../../components/EmptyState'
import ActionButtons from '../../components/ActionButtons' import ActionButtons from '../../components/ActionButtons'
import StatusDot from '../../components/StatusDot'
import type { WGInterface } from './types' import type { WGInterface } from './types'
const { Text } = Typography const { Text } = Typography
@@ -76,6 +75,14 @@ export default function ClientsTab() {
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'clients'] }) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'clients'] }) },
onError: (e: Error) => message.error(e.message), onError: (e: Error) => message.error(e.message),
}) })
const quickToggle = useMutation({
mutationFn: async ({ id, row, checked }: { id: number; row: WGInterface; checked: boolean }) => {
const { id: _id, created_at: _ca, updated_at: _ua, ...body } = row
await apiClient.put(`/wireguard/interfaces/${id}`, { ...body, active: checked })
},
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'clients'] }) },
onError: (e: Error) => message.error(e.message),
})
const cols: ColumnsType<WGInterface> = [ const cols: ColumnsType<WGInterface> = [
{ title: t('wg.iface.name'), dataIndex: 'name', key: 'name', render: (s: string) => <code>{s}</code> }, { title: t('wg.iface.name'), dataIndex: 'name', key: 'name', render: (s: string) => <code>{s}</code> },
@@ -86,7 +93,17 @@ export default function ClientsTab() {
render: (k?: string | null) => k ? <Text code copyable={{ text: k }} style={{ fontSize: 11 }}>{k.slice(0, 16)}</Text> : '—', render: (k?: string | null) => k ? <Text code copyable={{ text: k }} style={{ fontSize: 11 }}>{k.slice(0, 16)}</Text> : '—',
}, },
{ title: t('wg.iface.zone'), dataIndex: 'role', key: 'role', render: (r: string) => <Tag>{r}</Tag> }, { title: t('wg.iface.zone'), dataIndex: 'role', key: 'role', render: (r: string) => <Tag>{r}</Tag> },
{ title: t('common.active'), dataIndex: 'active', key: 'active', render: (v: boolean) => <StatusDot active={v} /> }, {
title: t('common.active'), dataIndex: 'active', key: 'active', width: 80,
render: (v: boolean, row: WGInterface) => (
<Switch
size="small"
checked={v}
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
/>
),
},
{ {
title: t('common.actions'), key: 'actions', title: t('common.actions'), key: 'actions',
render: (_, row) => ( render: (_, row) => (

View File

@@ -128,6 +128,14 @@ export default function ServersTab() {
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'servers'] }) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'servers'] }) },
onError: (e: Error) => message.error(e.message), onError: (e: Error) => message.error(e.message),
}) })
const quickToggle = useMutation({
mutationFn: async ({ id, row, checked }: { id: number; row: WGInterface; checked: boolean }) => {
const { id: _id, created_at: _ca, updated_at: _ua, ...body } = row
await apiClient.put(`/wireguard/interfaces/${id}`, { ...body, active: checked })
},
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'servers'] }) },
onError: (e: Error) => message.error(e.message),
})
const cols: ColumnsType<WGInterface> = [ const cols: ColumnsType<WGInterface> = [
{ title: t('wg.iface.name'), dataIndex: 'name', key: 'name', render: (s: string) => <code>{s}</code> }, { title: t('wg.iface.name'), dataIndex: 'name', key: 'name', render: (s: string) => <code>{s}</code> },
@@ -138,7 +146,17 @@ export default function ServersTab() {
render: (k: string) => <Text code copyable={{ text: k }} style={{ fontSize: 11 }}>{k.slice(0, 16)}</Text>, render: (k: string) => <Text code copyable={{ text: k }} style={{ fontSize: 11 }}>{k.slice(0, 16)}</Text>,
}, },
{ title: t('wg.iface.zone'), dataIndex: 'role', key: 'role', render: (r: string) => <Tag>{r}</Tag> }, { title: t('wg.iface.zone'), dataIndex: 'role', key: 'role', render: (r: string) => <Tag>{r}</Tag> },
{ title: t('common.active'), dataIndex: 'active', key: 'active', render: (v: boolean) => <StatusDot active={v} /> }, {
title: t('common.active'), dataIndex: 'active', key: 'active', width: 80,
render: (v: boolean, row: WGInterface) => (
<Switch
size="small"
checked={v}
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
/>
),
},
{ {
title: t('common.actions'), key: 'actions', title: t('common.actions'), key: 'actions',
render: (_, row) => ( render: (_, row) => (
@@ -354,6 +372,17 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'peers', ifaceID] }) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'peers', ifaceID] }) },
onError: (e: Error) => message.error(e.message), onError: (e: Error) => message.error(e.message),
}) })
const peerToggle = useMutation({
mutationFn: async ({ id, row, checked }: { id: number; row: WGPeer; checked: boolean }) => {
await apiClient.put(`/wireguard/peers/${id}`, {
name: row.name, allowed_ips: row.allowed_ips,
keepalive: row.keepalive, enabled: checked,
description: row.description,
})
},
onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'peers', ifaceID] }) },
onError: (e: Error) => message.error(e.message),
})
const cols: ColumnsType<WGPeer> = [ const cols: ColumnsType<WGPeer> = [
{ title: t('wg.peer.name'), dataIndex: 'name', key: 'name' }, { title: t('wg.peer.name'), dataIndex: 'name', key: 'name' },
@@ -390,7 +419,17 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
return <Text style={{ fontSize: 11 }}>{fmtBytes(live.transfer_rx)} {fmtBytes(live.transfer_tx)}</Text> return <Text style={{ fontSize: 11 }}>{fmtBytes(live.transfer_rx)} {fmtBytes(live.transfer_tx)}</Text>
}, },
}, },
{ title: t('common.active'), dataIndex: 'enabled', key: 'enabled', render: (v: boolean) => <StatusDot active={v} /> }, {
title: t('common.active'), dataIndex: 'enabled', key: 'enabled', width: 80,
render: (v: boolean, row: WGPeer) => (
<Switch
size="small"
checked={v}
loading={peerToggle.isPending && peerToggle.variables?.id === row.id}
onChange={(checked) => peerToggle.mutate({ id: row.id, row, checked })}
/>
),
},
{ {
title: t('common.actions'), key: 'actions', title: t('common.actions'), key: 'actions',
render: (_, row) => ( render: (_, row) => (