♻️ refactor: Replace useQuery with Zustand store for subscription and node data management
This commit is contained in:
@@ -91,6 +91,7 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
|
||||
case 'mieru':
|
||||
return {
|
||||
type: 'mieru',
|
||||
enable: false,
|
||||
port: null,
|
||||
multiplex: 'none',
|
||||
transport: 'tcp',
|
||||
@@ -98,6 +99,7 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
|
||||
case 'anytls':
|
||||
return {
|
||||
type: 'anytls',
|
||||
enable: false,
|
||||
port: null,
|
||||
security: 'tls',
|
||||
padding_scheme: null,
|
||||
|
||||
@@ -18,7 +18,7 @@ export {
|
||||
} from './constants';
|
||||
|
||||
// Re-export all types
|
||||
export type { FieldConfig, ProtocolType, ServerFormValues } from './types';
|
||||
export type { FieldConfig, ProtocolType } from './types';
|
||||
|
||||
// Re-export all schemas
|
||||
export { formSchema, protocolApiScheme } from './schemas';
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import { protocols } from './constants';
|
||||
import { formSchema } from './schemas';
|
||||
|
||||
export type FieldConfig = {
|
||||
name: string;
|
||||
@@ -22,6 +20,4 @@ export type FieldConfig = {
|
||||
gridSpan?: 1 | 2;
|
||||
};
|
||||
|
||||
export type ServerFormValues = z.infer<typeof formSchema>;
|
||||
|
||||
export type ProtocolType = (typeof protocols)[number];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client';
|
||||
// Online users detail moved to separate component
|
||||
|
||||
import { ProTable, ProTableActions } from '@/components/pro-table';
|
||||
import {
|
||||
createServer,
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
resetSortWithServer,
|
||||
updateServer,
|
||||
} from '@/services/admin/server';
|
||||
import { useNode } from '@/store/node';
|
||||
import { useServer } from '@/store/server';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Badge } from '@workspace/ui/components/badge';
|
||||
import { Button } from '@workspace/ui/components/button';
|
||||
@@ -23,18 +25,6 @@ import OnlineUsersCell from './online-users-cell';
|
||||
import ServerConfig from './server-config';
|
||||
import ServerForm from './server-form';
|
||||
|
||||
type ProtocolName = 'shadowsocks' | 'vmess' | 'vless' | 'trojan' | 'hysteria2' | 'tuic' | 'anytls';
|
||||
|
||||
const PROTOCOL_COLORS: Record<ProtocolName, string> = {
|
||||
shadowsocks: 'bg-green-500',
|
||||
vmess: 'bg-rose-500',
|
||||
vless: 'bg-blue-500',
|
||||
trojan: 'bg-yellow-500',
|
||||
hysteria2: 'bg-purple-500',
|
||||
tuic: 'bg-cyan-500',
|
||||
anytls: 'bg-gray-500',
|
||||
};
|
||||
|
||||
function PctBar({ value }: { value: number }) {
|
||||
const v = value.toFixed(2);
|
||||
return (
|
||||
@@ -69,6 +59,8 @@ function RegionIpCell({
|
||||
|
||||
export default function ServersPage() {
|
||||
const t = useTranslations('servers');
|
||||
const { isServerReferencedByNodes } = useNode();
|
||||
const { fetchServers } = useServer();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [migrating, setMigrating] = useState(false);
|
||||
@@ -129,6 +121,7 @@ export default function ServersPage() {
|
||||
await createServer(values as unknown as API.CreateServerRequest);
|
||||
toast.success(t('created'));
|
||||
ref.current?.refresh();
|
||||
fetchServers();
|
||||
setLoading(false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -163,23 +156,16 @@ export default function ServersPage() {
|
||||
accessorKey: 'protocols',
|
||||
header: t('protocols'),
|
||||
cell: ({ row }) => {
|
||||
const list = (row.original.protocols || []) as API.Protocol[];
|
||||
if (!list.length) return t('noData');
|
||||
const list = row.original.protocols.filter(
|
||||
(p) => p.enable !== false,
|
||||
) as API.Protocol[];
|
||||
if (!list.length) return '—';
|
||||
return (
|
||||
<div className='flex flex-wrap gap-1'>
|
||||
{list.map((p, idx) => {
|
||||
const proto = ((p as any)?.type || '') as ProtocolName | '';
|
||||
if (!proto) return null;
|
||||
const color = PROTOCOL_COLORS[proto as ProtocolName];
|
||||
const port = (p as any)?.port as number | undefined;
|
||||
const label = `${proto}${port ? ` (${port})` : ''}`;
|
||||
return (
|
||||
<Badge
|
||||
key={idx}
|
||||
variant='outline'
|
||||
className={cn('text-primary-foreground', color)}
|
||||
>
|
||||
{label}
|
||||
<Badge key={idx} variant='outline'>
|
||||
{p.type} ({p.port})
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
@@ -272,6 +258,7 @@ export default function ServersPage() {
|
||||
});
|
||||
toast.success(t('updated'));
|
||||
ref.current?.refresh();
|
||||
fetchServers();
|
||||
setLoading(false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -282,13 +269,18 @@ export default function ServersPage() {
|
||||
/>,
|
||||
<ConfirmButton
|
||||
key='delete'
|
||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||
trigger={
|
||||
<Button variant='destructive' disabled={isServerReferencedByNodes(row.id)}>
|
||||
{t('delete')}
|
||||
</Button>
|
||||
}
|
||||
title={t('confirmDeleteTitle')}
|
||||
description={t('confirmDeleteDesc')}
|
||||
onConfirm={async () => {
|
||||
await deleteServer({ id: row.id } as any);
|
||||
toast.success(t('deleted'));
|
||||
ref.current?.refresh();
|
||||
fetchServers();
|
||||
}}
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
@@ -311,6 +303,7 @@ export default function ServersPage() {
|
||||
await createServer(body);
|
||||
toast.success(t('copied'));
|
||||
ref.current?.refresh();
|
||||
fetchServers();
|
||||
setLoading(false);
|
||||
}}
|
||||
>
|
||||
@@ -318,16 +311,22 @@ export default function ServersPage() {
|
||||
</Button>,
|
||||
],
|
||||
batchRender(rows) {
|
||||
const hasReferencedServers = rows.some((row) => isServerReferencedByNodes(row.id));
|
||||
return [
|
||||
<ConfirmButton
|
||||
key='delete'
|
||||
trigger={<Button variant='destructive'>{t('delete')}</Button>}
|
||||
trigger={
|
||||
<Button variant='destructive' disabled={hasReferencedServers}>
|
||||
{t('delete')}
|
||||
</Button>
|
||||
}
|
||||
title={t('confirmDeleteTitle')}
|
||||
description={t('confirmDeleteDesc')}
|
||||
onConfirm={async () => {
|
||||
await Promise.all(rows.map((r) => deleteServer({ id: r.id })));
|
||||
toast.success(t('deleted'));
|
||||
ref.current?.refresh();
|
||||
fetchServers();
|
||||
}}
|
||||
cancelText={t('cancel')}
|
||||
confirmText={t('confirm')}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useNode } from '@/store/node';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import {
|
||||
Accordion,
|
||||
@@ -48,7 +49,6 @@ import {
|
||||
getProtocolDefaultConfig,
|
||||
PROTOCOL_FIELDS,
|
||||
protocols as PROTOCOLS,
|
||||
ServerFormValues,
|
||||
} from './form-schema';
|
||||
|
||||
function DynamicField({
|
||||
@@ -321,14 +321,16 @@ export default function ServerForm(props: {
|
||||
trigger: string;
|
||||
title: string;
|
||||
loading?: boolean;
|
||||
initialValues?: Partial<ServerFormValues>;
|
||||
onSubmit: (values: ServerFormValues) => Promise<boolean> | boolean;
|
||||
initialValues?: Partial<API.Server>;
|
||||
onSubmit: (values: Partial<API.Server>) => Promise<boolean> | boolean;
|
||||
}) {
|
||||
const { trigger, title, loading, initialValues, onSubmit } = props;
|
||||
const t = useTranslations('servers');
|
||||
const [open, setOpen] = useState(false);
|
||||
const [accordionValue, setAccordionValue] = useState<string>();
|
||||
|
||||
const { isProtocolUsedInNodes } = useNode();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
@@ -337,7 +339,7 @@ export default function ServerForm(props: {
|
||||
country: '',
|
||||
city: '',
|
||||
ratio: 1,
|
||||
protocols: [],
|
||||
protocols: [] as any[],
|
||||
...initialValues,
|
||||
},
|
||||
});
|
||||
@@ -515,6 +517,7 @@ export default function ServerForm(props: {
|
||||
PROTOCOLS.findIndex((t) => t === type),
|
||||
);
|
||||
const current = (protocolsValues[i] || {}) as Record<string, any>;
|
||||
const isEnabled = current?.enable !== false;
|
||||
const fields = PROTOCOL_FIELDS[type] || [];
|
||||
return (
|
||||
<AccordionItem key={type} value={type} className='mb-2 rounded-lg border'>
|
||||
@@ -539,16 +542,20 @@ export default function ServerForm(props: {
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs',
|
||||
current.enable ? 'text-green-500' : 'text-muted-foreground',
|
||||
isEnabled ? 'text-green-500' : 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{current.enable ? t('enabled') : t('disabled')}
|
||||
{isEnabled ? t('enabled') : t('disabled')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
className='mr-2'
|
||||
checked={!!current.enable}
|
||||
checked={!!isEnabled}
|
||||
disabled={Boolean(
|
||||
initialValues?.id &&
|
||||
isProtocolUsedInNodes(initialValues?.id || 0, type),
|
||||
)}
|
||||
onCheckedChange={(checked) => {
|
||||
form.setValue(`protocols.${i}.enable`, checked);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user