import { useState } from 'react' import { Alert, Button, Card, Col, Form, Input, InputNumber, Modal, Row, Select, Switch, Tag, Typography, message, } from 'antd' import type { ColumnsType } from 'antd/es/table' import { KeyOutlined, PlusOutlined, ThunderboltOutlined } from '@ant-design/icons' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import apiClient, { isEnvelope } from '../../api/client' import DataTable from '../../components/DataTable' import EmptyState from '../../components/EmptyState' import ActionButtons from '../../components/ActionButtons' import type { WGInterface } from './types' const { Text } = Typography interface ClientForm { name: string address_cidr: string peer_endpoint: string peer_public_key: string peer_psk?: string allowed_ips: string persistent_keepalive?: number mtu?: number role: string active: boolean description?: string generate_keypair: boolean private_key?: string } async function listClients(): Promise { const r = await apiClient.get('/wireguard/interfaces') if (!isEnvelope(r.data)) return [] return ((r.data.data as { interfaces?: WGInterface[] }).interfaces ?? []) .filter(i => i.mode === 'client') } interface FwZoneLite { name: string; builtin: boolean } async function listZones(): Promise { const r = await apiClient.get('/firewall/zones') if (!isEnvelope(r.data)) return [] return (r.data.data as { zones?: FwZoneLite[] }).zones ?? [] } export default function ClientsTab() { const { t } = useTranslation() const qc = useQueryClient() const { data: clients, isLoading } = useQuery({ queryKey: ['wg', 'clients'], queryFn: listClients }) const { data: zones } = useQuery({ queryKey: ['fw-zones'], queryFn: listZones }) const [editing, setEditing] = useState(null) const [creating, setCreating] = useState(false) const [form] = Form.useForm() const upsert = useMutation({ mutationFn: async (v: ClientForm) => { const body = { ...v, mode: 'client' } if (editing) return (await apiClient.put(`/wireguard/interfaces/${editing.id}`, body)).data return (await apiClient.post('/wireguard/interfaces', body)).data }, onSuccess: () => { message.success(t('common.save')) setEditing(null); setCreating(false); form.resetFields() void qc.invalidateQueries({ queryKey: ['wg', 'clients'] }) }, onError: (e: Error) => message.error(e.message), }) const del = useMutation({ mutationFn: async (id: number) => { await apiClient.delete(`/wireguard/interfaces/${id}`) }, onSuccess: () => { void qc.invalidateQueries({ queryKey: ['wg', 'clients'] }) }, 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 = [ { title: t('wg.iface.name'), dataIndex: 'name', key: 'name', render: (s: string) => {s} }, { title: t('wg.iface.address'), dataIndex: 'address_cidr', key: 'address_cidr' }, { title: t('wg.iface.peerEndpoint'), dataIndex: 'peer_endpoint', key: 'peer_endpoint', render: (s?: string | null) => s ?? '—' }, { title: t('wg.iface.peerPublicKey'), dataIndex: 'peer_public_key', key: 'peer_public_key', render: (k?: string | null) => k ? {k.slice(0, 16)}… : '—', }, { title: t('wg.iface.zone'), dataIndex: 'role', key: 'role', render: (r: string) => {r} }, { title: t('common.active'), dataIndex: 'active', key: 'active', width: 80, render: (v: boolean, row: WGInterface) => ( quickToggle.mutate({ id: row.id, row, checked })} /> ), }, { title: t('common.actions'), key: 'actions', render: (_, row) => ( { setEditing(row) form.setFieldsValue({ name: row.name, address_cidr: row.address_cidr, peer_endpoint: row.peer_endpoint ?? '', peer_public_key: row.peer_public_key ?? '', allowed_ips: row.allowed_ips ?? '0.0.0.0/0,::/0', persistent_keepalive: row.persistent_keepalive ?? 25, mtu: row.mtu ?? undefined, role: row.role, active: row.active, description: row.description ?? undefined, generate_keypair: false, }) }} onDelete={() => del.mutate(row.id)} deleteConfirm={t('wg.iface.deleteConfirm', { name: row.name })} /> ), }, ] const openCreate = () => { setCreating(true); form.resetFields() form.setFieldsValue({ allowed_ips: '0.0.0.0/0,::/0', persistent_keepalive: 25, role: 'wan', active: true, generate_keypair: true, }) } return ( <> } onClick={openCreate}> {t('wg.iface.addClient')} } emptyContent={ } title={t('wg.iface.emptyClientTitle')} description={t('wg.iface.emptyClientDesc')} action={ } /> } /> { setEditing(null); setCreating(false); form.resetFields() }} onOk={() => { void form.submit() }} confirmLoading={upsert.isPending} width={680} destroyOnClose >
upsert.mutate(v)}>