🐛 fix: Remove unnecessary migration function code and add device configuration options
This commit is contained in:
parent
a46657d5ef
commit
521a7a97fb
@ -5,14 +5,11 @@ import {
|
||||
createServer,
|
||||
deleteServer,
|
||||
filterServerList,
|
||||
hasMigrateSeverNode,
|
||||
migrateServerNode,
|
||||
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';
|
||||
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||
@ -66,33 +63,6 @@ export default function ServersPage() {
|
||||
const [migrating, setMigrating] = useState(false);
|
||||
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 (
|
||||
<div className='space-y-4'>
|
||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
|
||||
@ -105,11 +75,6 @@ export default function ServersPage() {
|
||||
title: t('pageTitle'),
|
||||
toolbar: (
|
||||
<div className='flex gap-2'>
|
||||
{hasMigrate && (
|
||||
<Button variant='outline' onClick={handleMigrate} disabled={migrating}>
|
||||
{migrating ? t('migrating') : t('migrate')}
|
||||
</Button>
|
||||
)}
|
||||
<ServerForm
|
||||
trigger={t('create')}
|
||||
title={t('drawerCreateTitle')}
|
||||
|
||||
@ -43,6 +43,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
import { SS_CIPHERS } from './form-schema';
|
||||
|
||||
const dnsConfigSchema = z.object({
|
||||
proto: z.string(), // z.enum(['tcp', 'udp', 'tls', 'https', 'quic']),
|
||||
@ -55,6 +56,7 @@ const outboundConfigSchema = z.object({
|
||||
protocol: z.string(),
|
||||
address: z.string(),
|
||||
port: z.number(),
|
||||
cipher: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
rules: z.array(z.string()).optional(),
|
||||
});
|
||||
@ -92,7 +94,7 @@ export default function ServerConfig() {
|
||||
node_pull_interval: undefined,
|
||||
node_push_interval: undefined,
|
||||
traffic_report_threshold: undefined,
|
||||
ip_strategy: undefined,
|
||||
ip_strategy: 'prefer_ipv4',
|
||||
dns: [],
|
||||
block: [],
|
||||
outbound: [],
|
||||
@ -106,7 +108,8 @@ export default function ServerConfig() {
|
||||
node_pull_interval: cfgResp.node_pull_interval as number | undefined,
|
||||
node_push_interval: cfgResp.node_push_interval as number | undefined,
|
||||
traffic_report_threshold: cfgResp.traffic_report_threshold as number | undefined,
|
||||
ip_strategy: cfgResp.ip_strategy as 'prefer_ipv4' | 'prefer_ipv6' | undefined,
|
||||
ip_strategy:
|
||||
(cfgResp.ip_strategy as 'prefer_ipv4' | 'prefer_ipv6' | undefined) || 'prefer_ipv4',
|
||||
dns: cfgResp.dns || [],
|
||||
block: cfgResp.block || [],
|
||||
outbound: cfgResp.outbound || [],
|
||||
@ -364,89 +367,104 @@ export default function ServerConfig() {
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='outbound'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<ArrayInput
|
||||
className='grid grid-cols-2 gap-2'
|
||||
fields={[
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
className: 'col-span-2',
|
||||
placeholder: t('server_config.fields.outbound_name_placeholder'),
|
||||
},
|
||||
{
|
||||
name: 'protocol',
|
||||
type: 'select',
|
||||
placeholder: t(
|
||||
'server_config.fields.outbound_protocol_placeholder',
|
||||
),
|
||||
options: [
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'SOCKS', value: 'socks' },
|
||||
{ label: 'Shadowsocks', value: 'shadowsocks' },
|
||||
{ label: 'Brook', value: 'brook' },
|
||||
{ label: 'Snell', value: 'snell' },
|
||||
{ label: 'VMess', value: 'vmess' },
|
||||
{ label: 'VLESS', value: 'vless' },
|
||||
{ label: 'Trojan', value: 'trojan' },
|
||||
{ label: 'WireGuard', value: 'wireguard' },
|
||||
{ label: 'Hysteria', value: 'hysteria' },
|
||||
{ label: 'TUIC', value: 'tuic' },
|
||||
{ label: 'AnyTLS', value: 'anytls' },
|
||||
{ label: 'Naive', value: 'naive' },
|
||||
{ label: 'Direct', value: 'direct' },
|
||||
{ label: 'Reject', value: 'reject' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
type: 'text',
|
||||
placeholder: t('server_config.fields.outbound_address_placeholder'),
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
placeholder: t('server_config.fields.outbound_port_placeholder'),
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
placeholder: t(
|
||||
'server_config.fields.outbound_password_placeholder',
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'rules',
|
||||
type: 'textarea',
|
||||
className: 'col-span-2',
|
||||
placeholder: t('server_config.fields.outbound_rules_placeholder'),
|
||||
},
|
||||
]}
|
||||
value={(field.value || []).map((item) => ({
|
||||
...item,
|
||||
rules: Array.isArray(item.rules) ? item.rules.join('\n') : '',
|
||||
}))}
|
||||
onChange={(values) => {
|
||||
const converted = values.map((item: any) => ({
|
||||
name: item.name,
|
||||
protocol: item.protocol,
|
||||
address: item.address,
|
||||
port: item.port,
|
||||
password: item.password,
|
||||
rules:
|
||||
typeof item.rules === 'string'
|
||||
? item.rules.split('\n').map((r: string) => r.trim())
|
||||
: item.rules || [],
|
||||
}));
|
||||
field.onChange(converted);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<ArrayInput
|
||||
className='grid grid-cols-2 gap-2'
|
||||
fields={[
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
className: 'col-span-2',
|
||||
placeholder: t('server_config.fields.outbound_name_placeholder'),
|
||||
},
|
||||
{
|
||||
name: 'protocol',
|
||||
type: 'select',
|
||||
placeholder: t(
|
||||
'server_config.fields.outbound_protocol_placeholder',
|
||||
),
|
||||
options: [
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'SOCKS', value: 'socks' },
|
||||
{ label: 'Shadowsocks', value: 'shadowsocks' },
|
||||
{ label: 'Brook', value: 'brook' },
|
||||
{ label: 'Snell', value: 'snell' },
|
||||
{ label: 'VMess', value: 'vmess' },
|
||||
{ label: 'VLESS', value: 'vless' },
|
||||
{ label: 'Trojan', value: 'trojan' },
|
||||
{ label: 'WireGuard', value: 'wireguard' },
|
||||
{ label: 'Hysteria', value: 'hysteria' },
|
||||
{ label: 'TUIC', value: 'tuic' },
|
||||
{ label: 'AnyTLS', value: 'anytls' },
|
||||
{ label: 'Naive', value: 'naive' },
|
||||
{ label: 'Direct', value: 'direct' },
|
||||
{ label: 'Reject', value: 'reject' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'cipher',
|
||||
type: 'select',
|
||||
options: SS_CIPHERS.map((cipher) => ({
|
||||
label: cipher,
|
||||
value: cipher,
|
||||
})),
|
||||
visible: (item: Record<string, any>) =>
|
||||
item.protocol === 'shadowsocks',
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
type: 'text',
|
||||
placeholder: t(
|
||||
'server_config.fields.outbound_address_placeholder',
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'port',
|
||||
type: 'number',
|
||||
placeholder: t('server_config.fields.outbound_port_placeholder'),
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
placeholder: t(
|
||||
'server_config.fields.outbound_password_placeholder',
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'rules',
|
||||
type: 'textarea',
|
||||
className: 'col-span-2',
|
||||
placeholder: t('server_config.fields.outbound_rules_placeholder'),
|
||||
},
|
||||
]}
|
||||
value={(field.value || []).map((item) => ({
|
||||
...item,
|
||||
rules: Array.isArray(item.rules) ? item.rules.join('\n') : '',
|
||||
}))}
|
||||
onChange={(values) => {
|
||||
const converted = values.map((item: any) => ({
|
||||
name: item.name,
|
||||
protocol: item.protocol,
|
||||
address: item.address,
|
||||
port: item.port,
|
||||
cipher: item.cipher,
|
||||
password: item.password,
|
||||
rules:
|
||||
typeof item.rules === 'string'
|
||||
? item.rules.split('\n').map((r: string) => r.trim())
|
||||
: item.rules || [],
|
||||
}));
|
||||
field.onChange(converted);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@ -45,6 +45,12 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
ip_register_limit: 0,
|
||||
ip_register_limit_duration: 0,
|
||||
},
|
||||
device: {
|
||||
enable: false,
|
||||
show_ads: false,
|
||||
enable_security: false,
|
||||
only_real_device: false,
|
||||
},
|
||||
},
|
||||
invite: {
|
||||
forced_invite: false,
|
||||
|
||||
@ -48,6 +48,12 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
ip_register_limit: 0,
|
||||
ip_register_limit_duration: 0,
|
||||
},
|
||||
device: {
|
||||
enable: false,
|
||||
show_ads: false,
|
||||
enable_security: false,
|
||||
only_real_device: false,
|
||||
},
|
||||
},
|
||||
invite: {
|
||||
forced_invite: false,
|
||||
|
||||
@ -12,6 +12,8 @@ interface FieldConfig extends Omit<EnhancedInputProps, 'type'> {
|
||||
name: string;
|
||||
type: 'text' | 'number' | 'select' | 'time' | 'boolean' | 'textarea';
|
||||
options?: { label: string; value: string }[];
|
||||
// optional per-item visibility function: returns true to show the field for the given item
|
||||
visible?: (item: Record<string, any>) => boolean;
|
||||
}
|
||||
|
||||
interface ObjectInputProps<T> {
|
||||
@ -40,6 +42,8 @@ export function ObjectInput<T extends Record<string, any>>({
|
||||
onChange(updatedInternalState);
|
||||
};
|
||||
const renderField = (field: FieldConfig) => {
|
||||
// if visible callback exists and returns false for current item, don't render
|
||||
if (field.visible && !field.visible(internalState)) return null;
|
||||
switch (field.type) {
|
||||
case 'select':
|
||||
return (
|
||||
@ -86,11 +90,15 @@ export function ObjectInput<T extends Record<string, any>>({
|
||||
};
|
||||
return (
|
||||
<div className={cn('flex flex-1 flex-wrap gap-4', className)}>
|
||||
{fields.map((field) => (
|
||||
<div key={field.name} className={cn('flex-1', field.className)}>
|
||||
{renderField(field)}
|
||||
</div>
|
||||
))}
|
||||
{fields.map((field) => {
|
||||
const node = renderField(field);
|
||||
if (node === null) return null; // don't render wrapper if field hidden
|
||||
return (
|
||||
<div key={field.name} className={cn('flex-1', field.className)}>
|
||||
{node}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user