refactor(frontend): break down DnsTab columns/helpers/types

Extract DnsTab's pure pieces into the dns/ folder: helpers.ts
(STRATEGIES/DEFAULT_FAKEDNS + addr/domains/expectedIPs accessors),
types.ts (DnsConfig/HostRow/FakednsRow), and useDnsColumns.tsx
(useDnsServerColumns + useFakednsColumns table-column hooks taking their
row handlers as params). DnsTab stays the orchestrator for dns state,
mutate, hosts sync and the Collapse panels, and drops from 539 to 424
lines. No behavior change.
This commit is contained in:
MHSanaei 2026-05-30 20:52:57 +02:00
parent ff8044b411
commit b2660d43eb
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
4 changed files with 163 additions and 123 deletions

View file

@ -1,49 +1,24 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Collapse, Dropdown, Empty, Input, InputNumber, Modal, Select, Space, Switch, Table } from 'antd';
import { PlusOutlined, MoreOutlined, EditOutlined, DeleteOutlined, MenuOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { Button, Collapse, Empty, Input, InputNumber, Modal, Select, Space, Switch, Table } from 'antd';
import { PlusOutlined, DeleteOutlined, MenuOutlined } from '@ant-design/icons';
import { SettingListItem } from '@/components/ui';
import DnsServerModal from './DnsServerModal';
import type { DnsServerValue } from './DnsServerModal';
import DnsPresetsModal from './DnsPresetsModal';
import type { XraySettingsValue, SetTemplate } from '@/hooks/useXraySetting';
import { DnsQueryStrategySchema, type DnsObject } from '@/schemas/dns';
import './DnsTab.css';
import { STRATEGIES, DEFAULT_FAKEDNS } from './helpers';
import type { DnsConfig, HostRow, FakednsRow } from './types';
import { useDnsServerColumns, useFakednsColumns } from './useDnsColumns';
interface DnsTabProps {
templateSettings: XraySettingsValue | null;
setTemplateSettings: SetTemplate;
}
const STRATEGIES = DnsQueryStrategySchema.options;
const DEFAULT_FAKEDNS = () => ({ ipPool: '198.18.0.0/15', poolSize: 65535 });
type DnsConfig = Omit<DnsObject, 'servers'> & { servers?: DnsServerValue[] };
interface HostRow {
domain: string;
values: string[];
}
interface FakednsRow {
ipPool: string;
poolSize: number;
}
function addrFor(server: DnsServerValue): string {
return typeof server === 'string' ? server : server?.address || '';
}
function domainsFor(server: DnsServerValue): string {
return typeof server === 'object' && server !== null ? (server.domains || []).join(',') : '';
}
function expectedIPsFor(server: DnsServerValue): string {
if (typeof server !== 'object' || !server) return '';
const list = server.expectedIPs || server.expectIPs || [];
return Array.isArray(list) ? list.join(',') : '';
}
export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTabProps) {
const { t } = useTranslation();
const [modal, modalContextHolder] = Modal.useModal();
@ -142,52 +117,7 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
return list.map((server, idx) => ({ key: idx, server }));
}, [dns?.servers]);
const dnsColumns: ColumnsType<{ key: number; server: DnsServerValue }> = useMemo(
() => [
{
title: '#',
key: 'action',
align: 'center',
width: 60,
render: (_v, _record, index) => (
<Space size={6}>
<span className="row-index">{index + 1}</span>
<Dropdown
trigger={['click']}
menu={{
items: [
{ key: 'edit', label: <><EditOutlined /> {t('edit')}</>, onClick: () => openEditServer(index) },
{ key: 'del', danger: true, label: <><DeleteOutlined /> {t('delete')}</>, onClick: () => deleteServer(index) },
],
}}
>
<Button shape="circle" size="small" icon={<MoreOutlined />} />
</Dropdown>
</Space>
),
},
{
title: t('pages.inbounds.address'),
key: 'address',
align: 'left',
render: (_v, record) => addrFor(record.server),
},
{
title: t('pages.xray.dns.domains'),
key: 'domains',
align: 'left',
render: (_v, record) => <span className="muted">{domainsFor(record.server)}</span>,
},
{
title: t('pages.xray.dns.expectIPs'),
key: 'expectedIPs',
align: 'left',
render: (_v, record) => <span className="muted">{expectedIPsFor(record.server)}</span>,
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[t],
);
const dnsColumns = useDnsServerColumns({ openEditServer, deleteServer });
function openAddServer() {
setEditingServer(null);
@ -241,52 +171,7 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
return list.map((entry, idx) => ({ key: idx, ...entry }));
}, [templateSettings?.fakedns]);
const fakednsColumns: ColumnsType<{ key: number; ipPool: string; poolSize: number }> = useMemo(
() => [
{
title: '#',
key: 'action',
align: 'center',
width: 60,
render: (_v, _record, index) => (
<Space size={6}>
<span className="row-index">{index + 1}</span>
<Button shape="circle" size="small" danger icon={<DeleteOutlined />} onClick={() => deleteFakedns(index)} />
</Space>
),
},
{
title: 'IP pool',
dataIndex: 'ipPool',
key: 'ipPool',
align: 'left',
render: (_v, record, index) => (
<Input
value={record.ipPool}
size="small"
onChange={(e) => updateFakednsField(index, 'ipPool', e.target.value)}
/>
),
},
{
title: 'Pool size',
dataIndex: 'poolSize',
key: 'poolSize',
align: 'right',
width: 120,
render: (_v, record, index) => (
<InputNumber
value={record.poolSize}
min={1}
size="small"
onChange={(v) => updateFakednsField(index, 'poolSize', Number(v) || 0)}
/>
),
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const fakednsColumns = useFakednsColumns({ deleteFakedns, updateFakednsField });
function addFakedns() {
mutate((tt) => {

View file

@ -0,0 +1,19 @@
import { DnsQueryStrategySchema } from '@/schemas/dns';
import type { DnsServerValue } from './DnsServerModal';
export const STRATEGIES = DnsQueryStrategySchema.options;
export const DEFAULT_FAKEDNS = () => ({ ipPool: '198.18.0.0/15', poolSize: 65535 });
export function addrFor(server: DnsServerValue): string {
return typeof server === 'string' ? server : server?.address || '';
}
export function domainsFor(server: DnsServerValue): string {
return typeof server === 'object' && server !== null ? (server.domains || []).join(',') : '';
}
export function expectedIPsFor(server: DnsServerValue): string {
if (typeof server !== 'object' || !server) return '';
const list = server.expectedIPs || server.expectIPs || [];
return Array.isArray(list) ? list.join(',') : '';
}

View file

@ -0,0 +1,14 @@
import type { DnsObject } from '@/schemas/dns';
import type { DnsServerValue } from './DnsServerModal';
export type DnsConfig = Omit<DnsObject, 'servers'> & { servers?: DnsServerValue[] };
export interface HostRow {
domain: string;
values: string[];
}
export interface FakednsRow {
ipPool: string;
poolSize: number;
}

View file

@ -0,0 +1,122 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Dropdown, Input, InputNumber, Space } from 'antd';
import { MoreOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { addrFor, domainsFor, expectedIPsFor } from './helpers';
import type { DnsServerValue } from './DnsServerModal';
export type DnsServerRow = { key: number; server: DnsServerValue };
export type FakednsTableRow = { key: number; ipPool: string; poolSize: number };
export function useDnsServerColumns({
openEditServer,
deleteServer,
}: {
openEditServer: (idx: number) => void;
deleteServer: (idx: number) => void;
}): ColumnsType<DnsServerRow> {
const { t } = useTranslation();
return useMemo(
() => [
{
title: '#',
key: 'action',
align: 'center',
width: 60,
render: (_v, _record, index) => (
<Space size={6}>
<span className="row-index">{index + 1}</span>
<Dropdown
trigger={['click']}
menu={{
items: [
{ key: 'edit', label: <><EditOutlined /> {t('edit')}</>, onClick: () => openEditServer(index) },
{ key: 'del', danger: true, label: <><DeleteOutlined /> {t('delete')}</>, onClick: () => deleteServer(index) },
],
}}
>
<Button shape="circle" size="small" icon={<MoreOutlined />} />
</Dropdown>
</Space>
),
},
{
title: t('pages.inbounds.address'),
key: 'address',
align: 'left',
render: (_v, record) => addrFor(record.server),
},
{
title: t('pages.xray.dns.domains'),
key: 'domains',
align: 'left',
render: (_v, record) => <span className="muted">{domainsFor(record.server)}</span>,
},
{
title: t('pages.xray.dns.expectIPs'),
key: 'expectedIPs',
align: 'left',
render: (_v, record) => <span className="muted">{expectedIPsFor(record.server)}</span>,
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[t],
);
}
export function useFakednsColumns({
deleteFakedns,
updateFakednsField,
}: {
deleteFakedns: (idx: number) => void;
updateFakednsField: (idx: number, field: 'ipPool' | 'poolSize', value: string | number) => void;
}): ColumnsType<FakednsTableRow> {
return useMemo(
() => [
{
title: '#',
key: 'action',
align: 'center',
width: 60,
render: (_v, _record, index) => (
<Space size={6}>
<span className="row-index">{index + 1}</span>
<Button shape="circle" size="small" danger icon={<DeleteOutlined />} onClick={() => deleteFakedns(index)} />
</Space>
),
},
{
title: 'IP pool',
dataIndex: 'ipPool',
key: 'ipPool',
align: 'left',
render: (_v, record, index) => (
<Input
value={record.ipPool}
size="small"
onChange={(e) => updateFakednsField(index, 'ipPool', e.target.value)}
/>
),
},
{
title: 'Pool size',
dataIndex: 'poolSize',
key: 'poolSize',
align: 'right',
width: 120,
render: (_v, record, index) => (
<InputNumber
value={record.poolSize}
min={1}
size="small"
onChange={(v) => updateFakednsField(index, 'poolSize', Number(v) || 0)}
/>
),
},
],
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
}