import { useState } from 'react' import { Alert, Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, Tag, Tooltip, Typography, message, } from 'antd' import type { ColumnsType } from 'antd/es/table' import { ExperimentOutlined, PlusOutlined, } from '@ant-design/icons' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import dayjs from 'dayjs' import apiClient, { isEnvelope } from '../../api/client' const { Text } = Typography interface RemoteTarget { id: number name: string kind: 's3' | 'sftp' target_url: string settings: Record active: boolean last_upload_at?: string last_error?: string } interface FormValues { name: string kind: 's3' | 'sftp' target_url: string active: boolean // S3 endpoint?: string region?: string bucket?: string access_key?: string secret_key?: string path_prefix?: string use_ssl?: boolean // SFTP host?: string port?: number username?: string password?: string private_key?: string remote_dir?: string host_key_fingerprint?: string } function buildPayload(v: FormValues): RemoteTarget { const settings: Record = {} if (v.kind === 's3') { if (v.endpoint) settings.endpoint = v.endpoint if (v.region) settings.region = v.region if (v.bucket) settings.bucket = v.bucket if (v.access_key) settings.access_key = v.access_key if (v.secret_key && v.secret_key !== '***SET***') settings.secret_key = v.secret_key else if (v.secret_key === '***SET***') settings.secret_key = '***SET***' if (v.path_prefix) settings.path_prefix = v.path_prefix settings.use_ssl = !!v.use_ssl } else { if (v.host) settings.host = v.host if (v.port) settings.port = v.port if (v.username) settings.username = v.username if (v.password && v.password !== '***SET***') settings.password = v.password else if (v.password === '***SET***') settings.password = '***SET***' if (v.private_key && v.private_key !== '***SET***') settings.private_key = v.private_key else if (v.private_key === '***SET***') settings.private_key = '***SET***' if (v.remote_dir) settings.remote_dir = v.remote_dir if (v.host_key_fingerprint) settings.host_key_fingerprint = v.host_key_fingerprint } return { id: 0, name: v.name, kind: v.kind, target_url: v.target_url, settings, active: v.active, } } export default function RemoteTargetsTab() { const { t } = useTranslation() const qc = useQueryClient() const [msg, msgCtx] = message.useMessage() const list = useQuery({ queryKey: ['backup-remotes'], queryFn: async () => { const r = await apiClient.get('/backup-remotes') return isEnvelope(r.data) ? (r.data.data as { remotes: RemoteTarget[] }).remotes : [] }, }) const [edit, setEdit] = useState(null) const [creating, setCreating] = useState(false) const [form] = Form.useForm() const kind = Form.useWatch('kind', form) const create = useMutation({ mutationFn: async (v: FormValues) => { await apiClient.post('/backup-remotes', buildPayload(v)) }, onSuccess: () => { msg.success(t('common.save')); setCreating(false); form.resetFields() qc.invalidateQueries({ queryKey: ['backup-remotes'] }) }, onError: (e: Error) => msg.error(e.message), }) const update = useMutation({ mutationFn: async ({ id, v }: { id: number; v: FormValues }) => { await apiClient.put(`/backup-remotes/${id}`, buildPayload(v)) }, onSuccess: () => { msg.success(t('common.save')); setEdit(null); form.resetFields() qc.invalidateQueries({ queryKey: ['backup-remotes'] }) }, onError: (e: Error) => msg.error(e.message), }) const del = useMutation({ mutationFn: async (id: number) => { await apiClient.delete(`/backup-remotes/${id}`) }, onSuccess: () => { qc.invalidateQueries({ queryKey: ['backup-remotes'] }) }, onError: (e: Error) => msg.error(e.message), }) const test = useMutation({ mutationFn: async (id: number) => { await apiClient.post(`/backup-remotes/${id}/test`) }, onSuccess: () => msg.success(t('remotes.testOk')), onError: (e: Error) => msg.error(t('remotes.testFailed') + ': ' + e.message), }) const columns: ColumnsType = [ { title: t('remotes.col.name'), dataIndex: 'name' }, { title: t('remotes.col.kind'), dataIndex: 'kind', width: 80, render: (v: string) => {v.toUpperCase()}, }, { title: t('remotes.col.target'), dataIndex: 'target_url', render: (v: string) => {v}, }, { title: t('remotes.col.lastUpload'), dataIndex: 'last_upload_at', width: 200, render: (v?: string, row?) => { if (!v) return const failed = !!row?.last_error return ( {dayjs(v).format('YYYY-MM-DD HH:mm:ss')} {failed && FAIL} ) }, }, { title: t('remotes.col.active'), dataIndex: 'active', width: 80, render: (v: boolean) => v ? an : aus, }, { title: t('common.actions'), key: 'a', width: 240, render: (_, r) => ( del.mutate(r.id)}> ), }, ] return (
{msgCtx} } onClick={() => { setCreating(true); form.resetFields() form.setFieldsValue({ kind: 's3', active: true, use_ssl: true, port: 22 }) }}> {t('remotes.add')} }> { setEdit(null); setCreating(false); form.resetFields() }} onOk={() => form.submit()} confirmLoading={create.isPending || update.isPending} width={640} >
edit ? update.mutate({ id: edit.id, v }) : create.mutate(v)}> {kind === 's3' && ( <> )} {kind === 'sftp' && ( <> )}
) }