mirror of
https://github.com/perfect-panel/ppanel-web.git
synced 2026-02-15 04:41:10 -05:00
🐛 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,
|
createServer,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
filterServerList,
|
filterServerList,
|
||||||
hasMigrateSeverNode,
|
|
||||||
migrateServerNode,
|
|
||||||
resetSortWithServer,
|
resetSortWithServer,
|
||||||
updateServer,
|
updateServer,
|
||||||
} from '@/services/admin/server';
|
} from '@/services/admin/server';
|
||||||
import { useNode } from '@/store/node';
|
import { useNode } from '@/store/node';
|
||||||
import { useServer } from '@/store/server';
|
import { useServer } from '@/store/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 { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
import { ConfirmButton } from '@workspace/ui/custom-components/confirm-button';
|
||||||
@ -66,33 +63,6 @@ export default function ServersPage() {
|
|||||||
const [migrating, setMigrating] = 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'>
|
||||||
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
|
<div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
|
||||||
@ -105,11 +75,6 @@ export default function ServersPage() {
|
|||||||
title: t('pageTitle'),
|
title: t('pageTitle'),
|
||||||
toolbar: (
|
toolbar: (
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
{hasMigrate && (
|
|
||||||
<Button variant='outline' onClick={handleMigrate} disabled={migrating}>
|
|
||||||
{migrating ? t('migrating') : t('migrate')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<ServerForm
|
<ServerForm
|
||||||
trigger={t('create')}
|
trigger={t('create')}
|
||||||
title={t('drawerCreateTitle')}
|
title={t('drawerCreateTitle')}
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import { useEffect, 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';
|
||||||
|
import { SS_CIPHERS } from './form-schema';
|
||||||
|
|
||||||
const dnsConfigSchema = z.object({
|
const dnsConfigSchema = z.object({
|
||||||
proto: z.string(), // z.enum(['tcp', 'udp', 'tls', 'https', 'quic']),
|
proto: z.string(), // z.enum(['tcp', 'udp', 'tls', 'https', 'quic']),
|
||||||
@ -55,6 +56,7 @@ const outboundConfigSchema = z.object({
|
|||||||
protocol: z.string(),
|
protocol: z.string(),
|
||||||
address: z.string(),
|
address: z.string(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
|
cipher: z.string().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
rules: z.array(z.string()).optional(),
|
rules: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
@ -92,7 +94,7 @@ export default function ServerConfig() {
|
|||||||
node_pull_interval: undefined,
|
node_pull_interval: undefined,
|
||||||
node_push_interval: undefined,
|
node_push_interval: undefined,
|
||||||
traffic_report_threshold: undefined,
|
traffic_report_threshold: undefined,
|
||||||
ip_strategy: undefined,
|
ip_strategy: 'prefer_ipv4',
|
||||||
dns: [],
|
dns: [],
|
||||||
block: [],
|
block: [],
|
||||||
outbound: [],
|
outbound: [],
|
||||||
@ -106,7 +108,8 @@ export default function ServerConfig() {
|
|||||||
node_pull_interval: cfgResp.node_pull_interval as number | undefined,
|
node_pull_interval: cfgResp.node_pull_interval as number | undefined,
|
||||||
node_push_interval: cfgResp.node_push_interval as number | undefined,
|
node_push_interval: cfgResp.node_push_interval as number | undefined,
|
||||||
traffic_report_threshold: cfgResp.traffic_report_threshold 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 || [],
|
dns: cfgResp.dns || [],
|
||||||
block: cfgResp.block || [],
|
block: cfgResp.block || [],
|
||||||
outbound: cfgResp.outbound || [],
|
outbound: cfgResp.outbound || [],
|
||||||
@ -364,89 +367,104 @@ export default function ServerConfig() {
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name='outbound'
|
name='outbound'
|
||||||
render={({ field }) => (
|
render={({ field }) => {
|
||||||
<FormItem>
|
return (
|
||||||
<FormControl>
|
<FormItem>
|
||||||
<ArrayInput
|
<FormControl>
|
||||||
className='grid grid-cols-2 gap-2'
|
<ArrayInput
|
||||||
fields={[
|
className='grid grid-cols-2 gap-2'
|
||||||
{
|
fields={[
|
||||||
name: 'name',
|
{
|
||||||
type: 'text',
|
name: 'name',
|
||||||
className: 'col-span-2',
|
type: 'text',
|
||||||
placeholder: t('server_config.fields.outbound_name_placeholder'),
|
className: 'col-span-2',
|
||||||
},
|
placeholder: t('server_config.fields.outbound_name_placeholder'),
|
||||||
{
|
},
|
||||||
name: 'protocol',
|
{
|
||||||
type: 'select',
|
name: 'protocol',
|
||||||
placeholder: t(
|
type: 'select',
|
||||||
'server_config.fields.outbound_protocol_placeholder',
|
placeholder: t(
|
||||||
),
|
'server_config.fields.outbound_protocol_placeholder',
|
||||||
options: [
|
),
|
||||||
{ label: 'HTTP', value: 'http' },
|
options: [
|
||||||
{ label: 'SOCKS', value: 'socks' },
|
{ label: 'HTTP', value: 'http' },
|
||||||
{ label: 'Shadowsocks', value: 'shadowsocks' },
|
{ label: 'SOCKS', value: 'socks' },
|
||||||
{ label: 'Brook', value: 'brook' },
|
{ label: 'Shadowsocks', value: 'shadowsocks' },
|
||||||
{ label: 'Snell', value: 'snell' },
|
{ label: 'Brook', value: 'brook' },
|
||||||
{ label: 'VMess', value: 'vmess' },
|
{ label: 'Snell', value: 'snell' },
|
||||||
{ label: 'VLESS', value: 'vless' },
|
{ label: 'VMess', value: 'vmess' },
|
||||||
{ label: 'Trojan', value: 'trojan' },
|
{ label: 'VLESS', value: 'vless' },
|
||||||
{ label: 'WireGuard', value: 'wireguard' },
|
{ label: 'Trojan', value: 'trojan' },
|
||||||
{ label: 'Hysteria', value: 'hysteria' },
|
{ label: 'WireGuard', value: 'wireguard' },
|
||||||
{ label: 'TUIC', value: 'tuic' },
|
{ label: 'Hysteria', value: 'hysteria' },
|
||||||
{ label: 'AnyTLS', value: 'anytls' },
|
{ label: 'TUIC', value: 'tuic' },
|
||||||
{ label: 'Naive', value: 'naive' },
|
{ label: 'AnyTLS', value: 'anytls' },
|
||||||
{ label: 'Direct', value: 'direct' },
|
{ label: 'Naive', value: 'naive' },
|
||||||
{ label: 'Reject', value: 'reject' },
|
{ label: 'Direct', value: 'direct' },
|
||||||
],
|
{ label: 'Reject', value: 'reject' },
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: 'address',
|
{
|
||||||
type: 'text',
|
name: 'cipher',
|
||||||
placeholder: t('server_config.fields.outbound_address_placeholder'),
|
type: 'select',
|
||||||
},
|
options: SS_CIPHERS.map((cipher) => ({
|
||||||
{
|
label: cipher,
|
||||||
name: 'port',
|
value: cipher,
|
||||||
type: 'number',
|
})),
|
||||||
placeholder: t('server_config.fields.outbound_port_placeholder'),
|
visible: (item: Record<string, any>) =>
|
||||||
},
|
item.protocol === 'shadowsocks',
|
||||||
{
|
},
|
||||||
name: 'password',
|
{
|
||||||
type: 'text',
|
name: 'address',
|
||||||
placeholder: t(
|
type: 'text',
|
||||||
'server_config.fields.outbound_password_placeholder',
|
placeholder: t(
|
||||||
),
|
'server_config.fields.outbound_address_placeholder',
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
name: 'rules',
|
{
|
||||||
type: 'textarea',
|
name: 'port',
|
||||||
className: 'col-span-2',
|
type: 'number',
|
||||||
placeholder: t('server_config.fields.outbound_rules_placeholder'),
|
placeholder: t('server_config.fields.outbound_port_placeholder'),
|
||||||
},
|
},
|
||||||
]}
|
{
|
||||||
value={(field.value || []).map((item) => ({
|
name: 'password',
|
||||||
...item,
|
type: 'text',
|
||||||
rules: Array.isArray(item.rules) ? item.rules.join('\n') : '',
|
placeholder: t(
|
||||||
}))}
|
'server_config.fields.outbound_password_placeholder',
|
||||||
onChange={(values) => {
|
),
|
||||||
const converted = values.map((item: any) => ({
|
},
|
||||||
name: item.name,
|
{
|
||||||
protocol: item.protocol,
|
name: 'rules',
|
||||||
address: item.address,
|
type: 'textarea',
|
||||||
port: item.port,
|
className: 'col-span-2',
|
||||||
password: item.password,
|
placeholder: t('server_config.fields.outbound_rules_placeholder'),
|
||||||
rules:
|
},
|
||||||
typeof item.rules === 'string'
|
]}
|
||||||
? item.rules.split('\n').map((r: string) => r.trim())
|
value={(field.value || []).map((item) => ({
|
||||||
: item.rules || [],
|
...item,
|
||||||
}));
|
rules: Array.isArray(item.rules) ? item.rules.join('\n') : '',
|
||||||
field.onChange(converted);
|
}))}
|
||||||
}}
|
onChange={(values) => {
|
||||||
/>
|
const converted = values.map((item: any) => ({
|
||||||
</FormControl>
|
name: item.name,
|
||||||
<FormMessage />
|
protocol: item.protocol,
|
||||||
</FormItem>
|
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>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,12 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
|||||||
ip_register_limit: 0,
|
ip_register_limit: 0,
|
||||||
ip_register_limit_duration: 0,
|
ip_register_limit_duration: 0,
|
||||||
},
|
},
|
||||||
|
device: {
|
||||||
|
enable: false,
|
||||||
|
show_ads: false,
|
||||||
|
enable_security: false,
|
||||||
|
only_real_device: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
invite: {
|
invite: {
|
||||||
forced_invite: false,
|
forced_invite: false,
|
||||||
|
|||||||
@ -48,6 +48,12 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
|||||||
ip_register_limit: 0,
|
ip_register_limit: 0,
|
||||||
ip_register_limit_duration: 0,
|
ip_register_limit_duration: 0,
|
||||||
},
|
},
|
||||||
|
device: {
|
||||||
|
enable: false,
|
||||||
|
show_ads: false,
|
||||||
|
enable_security: false,
|
||||||
|
only_real_device: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
invite: {
|
invite: {
|
||||||
forced_invite: false,
|
forced_invite: false,
|
||||||
|
|||||||
@ -12,6 +12,8 @@ interface FieldConfig extends Omit<EnhancedInputProps, 'type'> {
|
|||||||
name: string;
|
name: string;
|
||||||
type: 'text' | 'number' | 'select' | 'time' | 'boolean' | 'textarea';
|
type: 'text' | 'number' | 'select' | 'time' | 'boolean' | 'textarea';
|
||||||
options?: { label: string; value: string }[];
|
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> {
|
interface ObjectInputProps<T> {
|
||||||
@ -40,6 +42,8 @@ export function ObjectInput<T extends Record<string, any>>({
|
|||||||
onChange(updatedInternalState);
|
onChange(updatedInternalState);
|
||||||
};
|
};
|
||||||
const renderField = (field: FieldConfig) => {
|
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) {
|
switch (field.type) {
|
||||||
case 'select':
|
case 'select':
|
||||||
return (
|
return (
|
||||||
@ -86,11 +90,15 @@ export function ObjectInput<T extends Record<string, any>>({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-1 flex-wrap gap-4', className)}>
|
<div className={cn('flex flex-1 flex-wrap gap-4', className)}>
|
||||||
{fields.map((field) => (
|
{fields.map((field) => {
|
||||||
<div key={field.name} className={cn('flex-1', field.className)}>
|
const node = renderField(field);
|
||||||
{renderField(field)}
|
if (node === null) return null; // don't render wrapper if field hidden
|
||||||
</div>
|
return (
|
||||||
))}
|
<div key={field.name} className={cn('flex-1', field.className)}>
|
||||||
|
{node}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user