refactor(inbounds): remove column sorter from inbound list

Drop the table header sorter on the inbounds page: the sortKey/sortOrder
state, the sortedInbounds memo and onChange handler, the per-column
sorterFor spreads, the SORT_FNS comparator map, and the now-unused
SortKey/SortOrder types. The list renders in DB order.
This commit is contained in:
MHSanaei 2026-05-31 22:01:10 +02:00
parent 998fa0dfe1
commit 61e8bed3e0
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
4 changed files with 13 additions and 82 deletions

View file

@ -25,11 +25,10 @@ import {
import { HttpUtil } from '@/utils';
import { SORT_FNS } from './helpers';
import { buildRowActionsMenu } from './RowActions';
import { useInboundColumns } from './useInboundColumns';
import InboundStatsModal from './InboundStatsModal';
import type { DBInboundRecord, GeneralAction, InboundListProps, RowAction, SortKey, SortOrder } from './types';
import type { DBInboundRecord, GeneralAction, InboundListProps, RowAction } from './types';
import './InboundList.css';
export default function InboundList({
@ -49,8 +48,6 @@ export default function InboundList({
onBulkDelete,
}: InboundListProps) {
const { t } = useTranslation();
const [sortKey, setSortKey] = useState<SortKey | null>(null);
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
const [statsRecord, setStatsRecord] = useState<DBInboundRecord | null>(null);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
@ -67,14 +64,6 @@ export default function InboundList({
}
}, []);
const sortedInbounds = useMemo(() => {
if (!sortKey || !sortOrder) return dbInbounds;
const fn = SORT_FNS[sortKey];
if (!fn) return dbInbounds;
const sorted = [...dbInbounds].sort((a, b) => fn(a, b, { nodesById, clientCount }));
return sortOrder === 'descend' ? sorted.reverse() : sorted;
}, [dbInbounds, sortKey, sortOrder, nodesById, clientCount]);
const hasAnyRemark = useMemo(
() => dbInbounds.some((i) => typeof i.remark === 'string' && i.remark.trim() !== ''),
[dbInbounds],
@ -89,11 +78,11 @@ export default function InboundList({
}, []);
const selectAll = useCallback((checked: boolean) => {
setSelectedRowKeys(checked ? sortedInbounds.map((i) => i.id) : []);
}, [sortedInbounds]);
setSelectedRowKeys(checked ? dbInbounds.map((i) => i.id) : []);
}, [dbInbounds]);
const allSelected = sortedInbounds.length > 0 && selectedRowKeys.length === sortedInbounds.length;
const someSelected = selectedRowKeys.length > 0 && selectedRowKeys.length < sortedInbounds.length;
const allSelected = dbInbounds.length > 0 && selectedRowKeys.length === dbInbounds.length;
const someSelected = selectedRowKeys.length > 0 && selectedRowKeys.length < dbInbounds.length;
const handleBulkDelete = useCallback(async () => {
const ok = await onBulkDelete(selectedRowKeys);
@ -108,8 +97,6 @@ export default function InboundList({
subEnable,
expireDiff,
trafficDiff,
sortKey,
sortOrder,
onRowAction,
onSwitchEnable,
});
@ -160,7 +147,7 @@ export default function InboundList({
<Space orientation="vertical" style={{ width: '100%' }}>
{isMobile ? (
<div className="inbound-cards">
{sortedInbounds.length === 0 ? (
{dbInbounds.length === 0 ? (
<div className="card-empty">
<ImportOutlined style={{ fontSize: 28, opacity: 0.5 }} />
<div>{t('noData')}</div>
@ -179,7 +166,7 @@ export default function InboundList({
<span className="bulk-count">{selectedRowKeys.length}</span>
)}
</div>
{sortedInbounds.map((record) => (
{dbInbounds.map((record) => (
<div key={record.id} className={`inbound-card${selectedRowKeys.includes(record.id) ? ' is-selected' : ''}`}>
<div className="card-head">
<Checkbox
@ -217,13 +204,13 @@ export default function InboundList({
) : (
<Table
columns={columns}
dataSource={sortedInbounds}
dataSource={dbInbounds}
rowKey={(r) => r.id}
rowSelection={{
selectedRowKeys,
onChange: (keys: Key[]) => setSelectedRowKeys(keys as number[]),
}}
pagination={paginationFor(sortedInbounds)}
pagination={paginationFor(dbInbounds)}
scroll={{ x: 1000 }}
style={{ marginTop: 10 }}
size="small"
@ -235,12 +222,6 @@ export default function InboundList({
</div>
),
}}
onChange={(_p, _f, sorter) => {
const single = Array.isArray(sorter) ? sorter[0] : sorter;
const colKey = (single?.columnKey || single?.field) as SortKey | undefined;
setSortKey(colKey || null);
setSortOrder((single?.order as SortOrder) || null);
}}
/>
)}
</Space>

View file

@ -1,8 +1,7 @@
import type { NodeRecord } from '@/api/queries/useNodesQuery';
import { isSSMultiUser } from '@/lib/xray/protocol-capabilities';
import { coerceInboundJsonField } from '@/models/dbinbound';
import type { ClientCountEntry, DBInboundRecord, SortKey, StreamHints } from './types';
import type { DBInboundRecord, StreamHints } from './types';
export function readStreamHints(streamSettings: unknown): StreamHints {
const stream = coerceInboundJsonField(streamSettings) as { network?: string; security?: string };
@ -88,19 +87,3 @@ export function showQrCodeMenu(dbInbound: DBInboundRecord): boolean {
}
return false;
}
export const SORT_FNS: Record<SortKey, (a: DBInboundRecord, b: DBInboundRecord, ctx: { nodesById: Map<number, NodeRecord>; clientCount: Record<number, ClientCountEntry> }) => number> = {
id: (a, b) => a.id - b.id,
enable: (a, b) => Number(a.enable) - Number(b.enable),
remark: (a, b) => (a.remark || '').localeCompare(b.remark || ''),
port: (a, b) => a.port - b.port,
protocol: (a, b) => a.protocol.localeCompare(b.protocol),
traffic: (a, b) => (a.up + a.down) - (b.up + b.down),
expiryTime: (a, b) => (a.expiryTime || Infinity) - (b.expiryTime || Infinity),
node: (a, b, ctx) => {
const nameA = ctx.nodesById.get(a.nodeId ?? -1)?.name ?? (a.nodeId == null ? '￿' : `node #${a.nodeId}`);
const nameB = ctx.nodesById.get(b.nodeId ?? -1)?.name ?? (b.nodeId == null ? '￿' : `node #${b.nodeId}`);
return nameA.localeCompare(nameB);
},
clients: (a, b, ctx) => (ctx.clientCount[a.id]?.clients || 0) - (ctx.clientCount[b.id]?.clients || 0),
};

View file

@ -74,16 +74,3 @@ export interface InboundListProps {
onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void;
onBulkDelete: (ids: number[]) => Promise<boolean>;
}
export type SortKey =
| 'id'
| 'enable'
| 'remark'
| 'port'
| 'protocol'
| 'traffic'
| 'expiryTime'
| 'node'
| 'clients';
export type SortOrder = 'ascend' | 'descend' | null;

View file

@ -1,4 +1,4 @@
import { useCallback, useMemo, type ReactElement } from 'react';
import { useMemo, type ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { Popover, Switch, Tag, type TableColumnType } from 'antd';
@ -16,7 +16,7 @@ import {
tunnelNetworkLabel,
mixedNetworkLabel,
} from './helpers';
import type { ClientCountEntry, DBInboundRecord, RowAction, SortKey, SortOrder } from './types';
import type { ClientCountEntry, DBInboundRecord, RowAction } from './types';
interface UseInboundColumnsParams {
hasAnyRemark: boolean;
@ -26,8 +26,6 @@ interface UseInboundColumnsParams {
subEnable: boolean;
expireDiff: number;
trafficDiff: number;
sortKey: SortKey | null;
sortOrder: SortOrder;
onRowAction: (action: { key: RowAction; dbInbound: DBInboundRecord }) => void;
onSwitchEnable: (dbInbound: DBInboundRecord, next: boolean) => void;
}
@ -40,21 +38,12 @@ export function useInboundColumns({
subEnable,
expireDiff,
trafficDiff,
sortKey,
sortOrder,
onRowAction,
onSwitchEnable,
}: UseInboundColumnsParams): TableColumnType<DBInboundRecord>[] {
const { t } = useTranslation();
const { datepicker } = useDatepicker();
const sorterFor = useCallback((key: SortKey) => ({
sorter: true as const,
showSorterTooltip: false,
sortOrder: sortKey === key ? sortOrder : null,
sortDirections: ['ascend' as const, 'descend' as const],
}), [sortKey, sortOrder]);
return useMemo(() => {
const cols: TableColumnType<DBInboundRecord>[] = [
{
@ -63,7 +52,6 @@ export function useInboundColumns({
key: 'id',
align: 'right',
width: 30,
...sorterFor('id'),
},
{
title: t('pages.inbounds.operate'),
@ -84,7 +72,6 @@ export function useInboundColumns({
key: 'enable',
align: 'center',
width: 35,
...sorterFor('enable'),
render: (_, record) => (
<Switch
checked={record.enable}
@ -101,7 +88,6 @@ export function useInboundColumns({
key: 'remark',
align: 'center',
width: 60,
...sorterFor('remark'),
});
}
@ -111,7 +97,6 @@ export function useInboundColumns({
key: 'node',
align: 'center',
width: 60,
...sorterFor('node'),
render: (_, record) => {
if (record.nodeId == null) {
return <Tag color="default">{t('pages.inbounds.localPanel')}</Tag>;
@ -134,14 +119,12 @@ export function useInboundColumns({
key: 'port',
align: 'center',
width: 40,
...sorterFor('port'),
},
{
title: t('pages.inbounds.protocol'),
key: 'protocol',
align: 'left',
width: 130,
...sorterFor('protocol'),
render: (_, record) => {
const tags: ReactElement[] = [<Tag key="p" color="purple">{record.protocol}</Tag>];
if (record.isWireguard || record.isHysteria) {
@ -170,7 +153,6 @@ export function useInboundColumns({
key: 'clients',
align: 'left',
width: 50,
...sorterFor('clients'),
render: (_, record) => {
const cc = clientCount[record.id];
if (!cc) return null;
@ -236,7 +218,6 @@ export function useInboundColumns({
key: 'traffic',
align: 'center',
width: 90,
...sorterFor('traffic'),
render: (_, record) => (
<Popover
content={(
@ -269,7 +250,6 @@ export function useInboundColumns({
key: 'expiryTime',
align: 'center',
width: 40,
...sorterFor('expiryTime'),
render: (_, record) => {
if (record.expiryTime > 0) {
return (
@ -286,5 +266,5 @@ export function useInboundColumns({
);
return cols;
}, [t, hasAnyRemark, hasActiveNode, nodesById, clientCount, subEnable, expireDiff, trafficDiff, datepicker, onRowAction, onSwitchEnable, sorterFor]);
}, [t, hasAnyRemark, hasActiveNode, nodesById, clientCount, subEnable, expireDiff, trafficDiff, datepicker, onRowAction, onSwitchEnable]);
}