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:
@@ -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) => (
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user