feat: Implement data migration functionality and update localization files

This commit is contained in:
web 2025-09-03 01:56:21 -07:00
parent 59faeab34a
commit 6d81bfdaeb
27 changed files with 161 additions and 32 deletions

View File

@ -26,7 +26,7 @@ import { Combobox } from '@workspace/ui/custom-components/combobox';
import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import TagInput from '@workspace/ui/custom-components/tag-input'; import TagInput from '@workspace/ui/custom-components/tag-input';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { z } from 'zod'; import { z } from 'zod';
@ -77,6 +77,7 @@ export default function NodeForm(props: {
const { trigger, title, loading, initialValues, onSubmit } = props; const { trigger, title, loading, initialValues, onSubmit } = props;
const t = useTranslations('nodes'); const t = useTranslations('nodes');
const Scheme = useMemo(() => buildSchema(t), [t]); const Scheme = useMemo(() => buildSchema(t), [t]);
const [open, setOpen] = useState(false);
const form = useForm<NodeFormValues>({ const form = useForm<NodeFormValues>({
resolver: zodResolver(Scheme), resolver: zodResolver(Scheme),
@ -159,12 +160,15 @@ export default function NodeForm(props: {
async function submit(values: NodeFormValues) { async function submit(values: NodeFormValues) {
const ok = await onSubmit(values); const ok = await onSubmit(values);
if (ok) form.reset(); if (ok) {
form.reset();
setOpen(false);
}
return ok; return ok;
} }
return ( return (
<Sheet> <Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button onClick={() => form.reset()}>{trigger}</Button> <Button onClick={() => form.reset()}>{trigger}</Button>
</SheetTrigger> </SheetTrigger>
@ -295,7 +299,7 @@ export default function NodeForm(props: {
</ScrollArea> </ScrollArea>
<SheetFooter className='flex-row justify-end gap-2 pt-3'> <SheetFooter className='flex-row justify-end gap-2 pt-3'>
<Button variant='outline' disabled={loading}> <Button variant='outline' disabled={loading} onClick={() => setOpen(false)}>
{t('cancel')} {t('cancel')}
</Button> </Button>
<Button <Button

View File

@ -5,9 +5,12 @@ import {
createServer, createServer,
deleteServer, deleteServer,
filterServerList, filterServerList,
hasMigrateSeverNode,
migrateServerNode,
resetSortWithServer, resetSortWithServer,
updateServer, updateServer,
} from '@/services/admin/server'; } from '@/services/admin/server';
import { useQuery } from '@tanstack/react-query';
import { Badge } from '@workspace/ui/components/badge'; import { Badge } from '@workspace/ui/components/badge';
import { Button } from '@workspace/ui/components/button'; import { Button } from '@workspace/ui/components/button';
import { Card, CardContent } from '@workspace/ui/components/card'; import { Card, CardContent } from '@workspace/ui/components/card';
@ -64,14 +67,40 @@ function RegionIpCell({
); );
} }
// OnlineUsersCell is now a standalone component
export default function ServersPage() { export default function ServersPage() {
const t = useTranslations('servers'); const t = useTranslations('servers');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [migrating, setMigrating] = useState(false);
const ref = useRef<ProTableActions>(null); const ref = useRef<ProTableActions>(null);
const { data: hasMigrate, refetch: refetchHasMigrate } = useQuery({
queryKey: ['hasMigrateSeverNode'],
queryFn: async () => {
const { data } = await hasMigrateSeverNode();
return data.data?.has_migrate;
},
});
const handleMigrate = async () => {
setMigrating(true);
try {
const { data } = await migrateServerNode();
const fail = data.data?.fail || 0;
if (fail > 0) {
toast.error(data.data?.message);
} else {
toast.success(t('migrated'));
}
refetchHasMigrate();
ref.current?.refresh();
} catch (error) {
toast.error(t('migrateFailed'));
} finally {
setMigrating(false);
}
};
return ( return (
<div className='space-y-4'> <div className='space-y-4'>
<Card> <Card>
@ -84,24 +113,31 @@ export default function ServersPage() {
header={{ header={{
title: t('pageTitle'), title: t('pageTitle'),
toolbar: ( toolbar: (
<ServerForm <div className='flex gap-2'>
trigger={t('create')} {!hasMigrate && (
title={t('drawerCreateTitle')} <Button variant='outline' onClick={handleMigrate} disabled={migrating}>
loading={loading} {migrating ? t('migrating') : t('migrate')}
onSubmit={async (values) => { </Button>
setLoading(true); )}
try { <ServerForm
await createServer(values as unknown as API.CreateServerRequest); trigger={t('create')}
toast.success(t('created')); title={t('drawerCreateTitle')}
ref.current?.refresh(); loading={loading}
setLoading(false); onSubmit={async (values) => {
return true; setLoading(true);
} catch (e) { try {
setLoading(false); await createServer(values as unknown as API.CreateServerRequest);
return false; toast.success(t('created'));
} ref.current?.refresh();
}} setLoading(false);
/> return true;
} catch (e) {
setLoading(false);
return false;
}
}}
/>
</div>
), ),
}} }}
columns={[ columns={[
@ -156,17 +192,16 @@ export default function ServersPage() {
id: 'status', id: 'status',
header: t('status'), header: t('status'),
cell: ({ row }) => { cell: ({ row }) => {
const s = (row.original.status ?? {}) as API.ServerStatus; const offline = row.original.status.status === 'offline';
const on = !!(Array.isArray(s.online) && s.online.length > 0);
return ( return (
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
<span <span
className={cn( className={cn(
'inline-block h-2.5 w-2.5 rounded-full', 'inline-block h-2.5 w-2.5 rounded-full',
on ? 'bg-emerald-500' : 'bg-zinc-400', offline ? 'bg-zinc-400' : 'bg-emerald-500',
)} )}
/> />
<span className='text-sm'>{on ? t('online') : t('offline')}</span> <span className='text-sm'>{offline ? t('offline') : t('online')}</span>
</div> </div>
); );
}, },

View File

@ -33,7 +33,7 @@ import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input';
import { Icon } from '@workspace/ui/custom-components/icon'; import { Icon } from '@workspace/ui/custom-components/icon';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useFieldArray, useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
FINGERPRINTS, FINGERPRINTS,
@ -99,7 +99,6 @@ export default function ServerForm<T extends { [x: string]: any }>({
const form = useForm<any>({ resolver: zodResolver(formSchema), defaultValues }); const form = useForm<any>({ resolver: zodResolver(formSchema), defaultValues });
const { control } = form; const { control } = form;
const { fields, append, remove } = useFieldArray({ control, name: 'protocols' });
const protocolsValues = useWatch({ control, name: 'protocols' }); const protocolsValues = useWatch({ control, name: 'protocols' });

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP adresy", "ipAddresses": "IP adresy",
"memory": "Paměť", "memory": "Paměť",
"migrate": "Migrace dat",
"migrateFailed": "Migrace dat se nezdařila",
"migrated": "Data byla úspěšně migrována",
"migrating": "Probíhá migrace...",
"name": "Název", "name": "Název",
"noData": "Žádná data", "noData": "Žádná data",
"notAvailable": "Není k dispozici", "notAvailable": "Není k dispozici",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP-Adressen", "ipAddresses": "IP-Adressen",
"memory": "Speicher", "memory": "Speicher",
"migrate": "Daten migrieren",
"migrateFailed": "Datenmigration fehlgeschlagen",
"migrated": "Daten erfolgreich migriert",
"migrating": "Wird migriert...",
"name": "Name", "name": "Name",
"noData": "Keine Daten", "noData": "Keine Daten",
"notAvailable": "Nicht verfügbar", "notAvailable": "Nicht verfügbar",

View File

@ -1,5 +1,4 @@
{ {
"address": "Address",
"address": "Address", "address": "Address",
"address_placeholder": "Server address", "address_placeholder": "Server address",
"cancel": "Cancel", "cancel": "Cancel",
@ -57,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP addresses", "ipAddresses": "IP addresses",
"memory": "Memory", "memory": "Memory",
"migrate": "Migrate Data",
"migrateFailed": "Data migration failed",
"migrated": "Data migrated successfully",
"migrating": "Migrating...",
"name": "Name", "name": "Name",
"noData": "No data", "noData": "No data",
"notAvailable": "N/A", "notAvailable": "N/A",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "Direcciones IP", "ipAddresses": "Direcciones IP",
"memory": "Memoria", "memory": "Memoria",
"migrate": "Migrar datos",
"migrateFailed": "La migración de datos falló",
"migrated": "Datos migrados con éxito",
"migrating": "Migrando...",
"name": "Nombre", "name": "Nombre",
"noData": "Sin datos", "noData": "Sin datos",
"notAvailable": "N/A", "notAvailable": "N/A",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "Direcciones IP", "ipAddresses": "Direcciones IP",
"memory": "Memoria", "memory": "Memoria",
"migrate": "Migrar datos",
"migrateFailed": "La migración de datos falló",
"migrated": "Datos migrados con éxito",
"migrating": "Migrando...",
"name": "Nombre", "name": "Nombre",
"noData": "Sin datos", "noData": "Sin datos",
"notAvailable": "N/A", "notAvailable": "N/A",

View File

@ -56,6 +56,10 @@
"id": "شناسه", "id": "شناسه",
"ipAddresses": "آدرس‌های IP", "ipAddresses": "آدرس‌های IP",
"memory": "حافظه", "memory": "حافظه",
"migrate": "انتقال داده",
"migrateFailed": "انتقال داده ناموفق بود",
"migrated": "داده با موفقیت منتقل شد",
"migrating": "در حال انتقال...",
"name": "نام", "name": "نام",
"noData": "هیچ داده‌ای", "noData": "هیچ داده‌ای",
"notAvailable": "غیرقابل دسترسی", "notAvailable": "غیرقابل دسترسی",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP-osoitteet", "ipAddresses": "IP-osoitteet",
"memory": "Muisti", "memory": "Muisti",
"migrate": "Siirrä tiedot",
"migrateFailed": "Tietojen siirto epäonnistui",
"migrated": "Tiedot siirretty onnistuneesti",
"migrating": "Siirretään...",
"name": "Nimi", "name": "Nimi",
"noData": "Ei tietoja", "noData": "Ei tietoja",
"notAvailable": "Ei saatavilla", "notAvailable": "Ei saatavilla",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "Adresses IP", "ipAddresses": "Adresses IP",
"memory": "Mémoire", "memory": "Mémoire",
"migrate": "Migrer les données",
"migrateFailed": "Échec de la migration des données",
"migrated": "Données migrées avec succès",
"migrating": "Migration en cours...",
"name": "Nom", "name": "Nom",
"noData": "Aucune donnée", "noData": "Aucune donnée",
"notAvailable": "N/A", "notAvailable": "N/A",

View File

@ -56,6 +56,10 @@
"id": "आईडी", "id": "आईडी",
"ipAddresses": "आईपी पते", "ipAddresses": "आईपी पते",
"memory": "मेमोरी", "memory": "मेमोरी",
"migrate": "डेटा माइग्रेट करें",
"migrateFailed": "डेटा माइग्रेशन विफल",
"migrated": "डेटा सफलतापूर्वक माइग्रेट किया गया",
"migrating": "माइग्रेट किया जा रहा है...",
"name": "नाम", "name": "नाम",
"noData": "कोई डेटा नहीं", "noData": "कोई डेटा नहीं",
"notAvailable": "उपलब्ध नहीं", "notAvailable": "उपलब्ध नहीं",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP címek", "ipAddresses": "IP címek",
"memory": "Memória", "memory": "Memória",
"migrate": "Adatok migrálása",
"migrateFailed": "Az adatok migrálása sikertelen",
"migrated": "Az adatok sikeresen migrálva",
"migrating": "Migrálás...",
"name": "Név", "name": "Név",
"noData": "Nincs adat", "noData": "Nincs adat",
"notAvailable": "N/A", "notAvailable": "N/A",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IPアドレス", "ipAddresses": "IPアドレス",
"memory": "メモリ", "memory": "メモリ",
"migrate": "データを移行する",
"migrateFailed": "データの移行に失敗しました",
"migrated": "データが正常に移行されました",
"migrating": "移行中...",
"name": "名前", "name": "名前",
"noData": "データなし", "noData": "データなし",
"notAvailable": "利用不可", "notAvailable": "利用不可",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP 주소", "ipAddresses": "IP 주소",
"memory": "메모리", "memory": "메모리",
"migrate": "데이터 마이그레이션",
"migrateFailed": "데이터 마이그레이션 실패",
"migrated": "데이터가 성공적으로 마이그레이션되었습니다",
"migrating": "마이그레이션 중...",
"name": "이름", "name": "이름",
"noData": "데이터 없음", "noData": "데이터 없음",
"notAvailable": "사용 불가", "notAvailable": "사용 불가",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP-adresser", "ipAddresses": "IP-adresser",
"memory": "Minne", "memory": "Minne",
"migrate": "Migrer data",
"migrateFailed": "Datamigrering mislyktes",
"migrated": "Data migrert med suksess",
"migrating": "Migrerer...",
"name": "Navn", "name": "Navn",
"noData": "Ingen data", "noData": "Ingen data",
"notAvailable": "Ikke tilgjengelig", "notAvailable": "Ikke tilgjengelig",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "Adresy IP", "ipAddresses": "Adresy IP",
"memory": "Pamięć", "memory": "Pamięć",
"migrate": "Migracja danych",
"migrateFailed": "Migracja danych nie powiodła się",
"migrated": "Dane zostały pomyślnie zmigrowane",
"migrating": "Migracja...",
"name": "Nazwa", "name": "Nazwa",
"noData": "Brak danych", "noData": "Brak danych",
"notAvailable": "N/D", "notAvailable": "N/D",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "Endereços IP", "ipAddresses": "Endereços IP",
"memory": "Memória", "memory": "Memória",
"migrate": "Migrar Dados",
"migrateFailed": "A migração de dados falhou",
"migrated": "Dados migrados com sucesso",
"migrating": "Migrando...",
"name": "Nome", "name": "Nome",
"noData": "Sem dados", "noData": "Sem dados",
"notAvailable": "N/D", "notAvailable": "N/D",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "Adrese IP", "ipAddresses": "Adrese IP",
"memory": "Memorie", "memory": "Memorie",
"migrate": "Migrați datele",
"migrateFailed": "Migrarea datelor a eșuat",
"migrated": "Datele au fost migrate cu succes",
"migrating": "Se migrează...",
"name": "Nume", "name": "Nume",
"noData": "Fără date", "noData": "Fără date",
"notAvailable": "N/A", "notAvailable": "N/A",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP-адреса", "ipAddresses": "IP-адреса",
"memory": "Память", "memory": "Память",
"migrate": "Перенести данные",
"migrateFailed": "Ошибка при переносе данных",
"migrated": "Данные успешно перенесены",
"migrating": "Перенос данных...",
"name": "Имя", "name": "Имя",
"noData": "Нет данных", "noData": "Нет данных",
"notAvailable": "Недоступно", "notAvailable": "Недоступно",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "ที่อยู่ IP", "ipAddresses": "ที่อยู่ IP",
"memory": "หน่วยความจำ", "memory": "หน่วยความจำ",
"migrate": "ย้ายข้อมูล",
"migrateFailed": "การย้ายข้อมูลล้มเหลว",
"migrated": "ย้ายข้อมูลสำเร็จ",
"migrating": "กำลังย้ายข้อมูล...",
"name": "ชื่อ", "name": "ชื่อ",
"noData": "ไม่มีข้อมูล", "noData": "ไม่มีข้อมูล",
"notAvailable": "ไม่สามารถใช้ได้", "notAvailable": "ไม่สามารถใช้ได้",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP adresleri", "ipAddresses": "IP adresleri",
"memory": "Bellek", "memory": "Bellek",
"migrate": "Veri Taşı",
"migrateFailed": "Veri taşıma işlemi başarısız oldu",
"migrated": "Veri başarıyla taşındı",
"migrating": "Taşınıyor...",
"name": "İsim", "name": "İsim",
"noData": "Veri yok", "noData": "Veri yok",
"notAvailable": "Mevcut değil", "notAvailable": "Mevcut değil",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP адреси", "ipAddresses": "IP адреси",
"memory": "Пам'ять", "memory": "Пам'ять",
"migrate": "Міграція даних",
"migrateFailed": "Міграція даних не вдалася",
"migrated": "Дані успішно мігрували",
"migrating": "Міграція...",
"name": "Ім'я", "name": "Ім'я",
"noData": "Немає даних", "noData": "Немає даних",
"notAvailable": "Н/Д", "notAvailable": "Н/Д",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "Địa chỉ IP", "ipAddresses": "Địa chỉ IP",
"memory": "Bộ nhớ", "memory": "Bộ nhớ",
"migrate": "Di chuyển dữ liệu",
"migrateFailed": "Di chuyển dữ liệu thất bại",
"migrated": "Dữ liệu đã được di chuyển thành công",
"migrating": "Đang di chuyển...",
"name": "Tên", "name": "Tên",
"noData": "Không có dữ liệu", "noData": "Không có dữ liệu",
"notAvailable": "Không khả dụng", "notAvailable": "Không khả dụng",

View File

@ -1,5 +1,4 @@
{ {
"address": "地址",
"address": "地址", "address": "地址",
"address_placeholder": "服务器地址", "address_placeholder": "服务器地址",
"cancel": "取消", "cancel": "取消",
@ -57,6 +56,10 @@
"id": "编号", "id": "编号",
"ipAddresses": "IP 地址", "ipAddresses": "IP 地址",
"memory": "内存", "memory": "内存",
"migrate": "迁移数据",
"migrateFailed": "数据迁移失败",
"migrated": "数据迁移成功",
"migrating": "迁移中...",
"name": "名称", "name": "名称",
"noData": "暂无数据", "noData": "暂无数据",
"notAvailable": "—", "notAvailable": "—",

View File

@ -56,6 +56,10 @@
"id": "ID", "id": "ID",
"ipAddresses": "IP 地址", "ipAddresses": "IP 地址",
"memory": "內存", "memory": "內存",
"migrate": "遷移數據",
"migrateFailed": "數據遷移失敗",
"migrated": "數據已成功遷移",
"migrating": "正在遷移...",
"name": "名稱", "name": "名稱",
"noData": "無數據", "noData": "無數據",
"notAvailable": "不可用", "notAvailable": "不可用",

View File

@ -1741,6 +1741,7 @@ declare namespace API {
disk: number; disk: number;
protocol: string; protocol: string;
online: ServerOnlineUser[]; online: ServerOnlineUser[];
status: string;
}; };
type ServerTotalDataResponse = { type ServerTotalDataResponse = {