import { useState } from 'react' import { Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Space, Switch, Table, Tag, Tooltip, Typography, message, } from 'antd' import type { ColumnsType } from 'antd/es/table' import { EnvironmentOutlined, PlusOutlined, ReloadOutlined, } from '@ant-design/icons' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import apiClient, { isEnvelope } from '../../api/client' import { useAuthStore } from '../../stores/auth' import EmptyState from '../../components/EmptyState' const { Text } = Typography interface ManagedRoute { id: number destination: string gateway?: string | null dev?: string | null metric: number table_name: string active: boolean comment?: string | null } interface LiveRoute { destination: string gateway?: string dev?: string protocol?: string scope?: string src?: string metric?: number table?: string flags?: string[] } interface RouteFormValues { destination: string gateway?: string dev?: string metric: number table_name: string active: boolean comment?: string } export default function RoutesTab() { const { t } = useTranslation() const qc = useQueryClient() const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const managed = useQuery({ queryKey: ['routes', 'managed'], queryFn: async () => { const r = await apiClient.get('/routes') return isEnvelope(r.data) ? (r.data.data as { routes: ManagedRoute[] }).routes : [] }, }) const live = useQuery({ queryKey: ['routes', 'live'], queryFn: async () => { const r = await apiClient.get('/routes/live') return isEnvelope(r.data) ? (r.data.data as { routes: LiveRoute[] }).routes : [] }, refetchInterval: 30_000, }) const [edit, setEdit] = useState(null) const [creating, setCreating] = useState(false) const [form] = Form.useForm() const create = useMutation({ mutationFn: async (v: RouteFormValues) => { await apiClient.post('/routes', v) }, onSuccess: () => { message.success(t('common.save')) setCreating(false); form.resetFields() qc.invalidateQueries({ queryKey: ['routes'] }) }, onError: (e: Error) => message.error(e.message), }) const update = useMutation({ mutationFn: async ({ id, v }: { id: number; v: RouteFormValues }) => { await apiClient.put(`/routes/${id}`, v) }, onSuccess: () => { message.success(t('common.save')) setEdit(null); form.resetFields() qc.invalidateQueries({ queryKey: ['routes'] }) }, onError: (e: Error) => message.error(e.message), }) const del = useMutation({ mutationFn: async (id: number) => { await apiClient.delete(`/routes/${id}`) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['routes'] }) }, onError: (e: Error) => message.error(e.message), }) const quickToggle = useMutation({ mutationFn: async ({ id, row, checked }: { id: number; row: ManagedRoute; checked: boolean }) => { const { id: _id, ...body } = row await apiClient.put(`/routes/${id}`, { ...body, active: checked }) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['routes'] }) }, onError: (e: Error) => message.error(e.message), }) const managedColumns: ColumnsType = [ { title: t('routes.col.destination'), dataIndex: 'destination', render: (v: string) => {v}, }, { title: t('routes.col.gateway'), dataIndex: 'gateway', render: (v?: string) => v ? {v} : on-link, }, { title: t('routes.col.dev'), dataIndex: 'dev', width: 110, render: (v?: string) => v ? {v} : '—' }, { title: t('routes.col.metric'), dataIndex: 'metric', width: 80 }, { title: t('routes.col.table'), dataIndex: 'table_name', width: 90 }, { title: t('routes.col.active'), dataIndex: 'active', width: 80, render: (v: boolean, row: ManagedRoute) => ( quickToggle.mutate({ id: row.id, row, checked })} /> ), }, { title: t('routes.col.comment'), dataIndex: 'comment', render: (v?: string) => v || }, { title: t('common.actions'), key: 'a', width: 160, render: (_, r) => ( {isViewer ? : del.mutate(r.id)}> } ), }, ] const liveColumns: ColumnsType = [ { title: t('routes.col.destination'), dataIndex: 'destination', render: (v?: string) => ( {v || 'default'} ) }, { title: t('routes.col.gateway'), dataIndex: 'gateway', render: (v?: string) => v ? {v} : }, { title: t('routes.col.dev'), dataIndex: 'dev', width: 110, render: (v?: string) => v ? {v} : '—' }, { title: t('routes.col.proto'), dataIndex: 'protocol', width: 110, render: (v?: string) => v ? {v} : '—' }, { title: t('routes.col.scope'), dataIndex: 'scope', width: 90, render: (v?: string) => v ? {v} : '—' }, { title: t('routes.col.src'), dataIndex: 'src', width: 130, render: (v?: string) => v ? {v} : }, { title: t('routes.col.metric'), dataIndex: 'metric', width: 80, render: (v?: number) => v ?? '—' }, ] const openCreate = () => { setCreating(true); form.resetFields() form.setFieldsValue({ metric: 100, table_name: 'main', active: true, destination: '', gateway: '', dev: '', }) } return (
{t('routes.liveTitle')} ({(live.data ?? []).length}) } extra={ } className="mb-16" > {t('routes.liveIntro')} `${r.destination}-${r.dev}-${r.metric}-${idx}`} size="small" dataSource={live.data ?? []} columns={liveColumns} pagination={{ defaultPageSize: 25, showSizeChanger: true, pageSizeOptions: [25, 50, 100] }} locale={{ emptyText: t('routes.liveEmpty') }} style={{ marginTop: 12 }} /> } > {t('routes.managedIntro')}
} title={t('routes.emptyTitle')} description={t('routes.emptyDesc')} action={ } /> ) }} style={{ marginTop: 12 }} /> { setEdit(null); setCreating(false); form.resetFields() }} onOk={() => form.submit()} confirmLoading={create.isPending || update.isPending} width={560} >
edit ? update.mutate({ id: edit.id, v }) : create.mutate(v)}>
) }