useOutboundColumns.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { useMemo } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Button, Dropdown, Tag, Tooltip } from 'antd';
  4. import {
  5. RetweetOutlined,
  6. MoreOutlined,
  7. EditOutlined,
  8. DeleteOutlined,
  9. VerticalAlignTopOutlined,
  10. ThunderboltOutlined,
  11. LoadingOutlined,
  12. ArrowUpOutlined,
  13. ArrowDownOutlined,
  14. } from '@ant-design/icons';
  15. import type { ColumnsType } from 'antd/es/table';
  16. import { SizeFormatter } from '@/utils';
  17. import { OutboundProtocols as Protocols } from '@/schemas/primitives';
  18. import { isUdpOutbound } from '@/hooks/useXraySetting';
  19. import type { OutboundTestState, OutboundTrafficRow } from '@/hooks/useXraySetting';
  20. import type { OutboundRow } from './outbounds-tab-types';
  21. import TestResultPopover from './TestResultPopover';
  22. import {
  23. isTesting,
  24. isUntestable,
  25. outboundAddresses,
  26. showSecurity,
  27. testResult,
  28. trafficFor,
  29. } from './outbounds-tab-helpers';
  30. interface OutboundColumnsParams {
  31. testMode: 'tcp' | 'http';
  32. rows: OutboundRow[];
  33. outboundsTraffic: OutboundTrafficRow[];
  34. outboundTestStates: Record<number, OutboundTestState>;
  35. openEdit: (idx: number) => void;
  36. setFirst: (idx: number) => void;
  37. moveUp: (idx: number) => void;
  38. moveDown: (idx: number) => void;
  39. confirmDelete: (idx: number) => void;
  40. onResetTraffic: (tag: string) => void;
  41. onTest: (index: number, mode: string) => void;
  42. }
  43. export function useOutboundColumns({
  44. testMode,
  45. rows,
  46. outboundsTraffic,
  47. outboundTestStates,
  48. openEdit,
  49. setFirst,
  50. moveUp,
  51. moveDown,
  52. confirmDelete,
  53. onResetTraffic,
  54. onTest,
  55. }: OutboundColumnsParams): ColumnsType<OutboundRow> {
  56. const { t } = useTranslation();
  57. return useMemo(
  58. () => [
  59. {
  60. title: '#',
  61. key: 'action',
  62. align: 'center',
  63. width: 100,
  64. render: (_v, _record, index) => (
  65. <div className="action-cell">
  66. <span className="row-index">{index + 1}</span>
  67. <div className="action-buttons">
  68. <Button shape="circle" size="small" icon={<EditOutlined />} aria-label={t('edit')} onClick={() => openEdit(index)} />
  69. <Dropdown
  70. trigger={['click']}
  71. menu={{
  72. items: [
  73. ...(index > 0
  74. ? [
  75. { key: 'top', label: <><VerticalAlignTopOutlined /> {t('pages.xray.outbound.moveToTop')}</>, onClick: () => setFirst(index) },
  76. ]
  77. : []),
  78. { key: 'up', label: <><ArrowUpOutlined /> {t('pages.inbounds.form.moveUp')}</>, disabled: index === 0, onClick: () => moveUp(index) },
  79. { key: 'down', label: <><ArrowDownOutlined /> {t('pages.inbounds.form.moveDown')}</>, disabled: index === rows.length - 1, onClick: () => moveDown(index) },
  80. { key: 'reset', label: <><RetweetOutlined /> {t('pages.inbounds.resetTraffic')}</>, onClick: () => onResetTraffic(rows[index].tag || '') },
  81. { key: 'del', danger: true, label: <><DeleteOutlined /> {t('delete')}</>, onClick: () => confirmDelete(index) },
  82. ],
  83. }}
  84. >
  85. <Button shape="circle" size="small" icon={<MoreOutlined />} aria-label={t('more')} />
  86. </Dropdown>
  87. </div>
  88. </div>
  89. ),
  90. },
  91. {
  92. title: t('pages.xray.outbound.tag'),
  93. key: 'identity',
  94. align: 'left',
  95. render: (_v, record) => (
  96. <div className="identity-cell">
  97. <Tooltip title={record.tag}>
  98. <span className="tag-name">{record.tag}</span>
  99. </Tooltip>
  100. <div className="protocol-line">
  101. <Tag color="green">{record.protocol}</Tag>
  102. {[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(record.protocol as never) && (
  103. <>
  104. <Tag>{record.streamSettings?.network}</Tag>
  105. {showSecurity(record.streamSettings?.security) && <Tag color="purple">{record.streamSettings?.security}</Tag>}
  106. </>
  107. )}
  108. </div>
  109. </div>
  110. ),
  111. },
  112. {
  113. title: t('pages.inbounds.address'),
  114. key: 'address',
  115. align: 'left',
  116. render: (_v, record) => {
  117. const addrs = outboundAddresses(record);
  118. return (
  119. <div className="address-list">
  120. {addrs.length === 0 ? (
  121. <span className="empty">—</span>
  122. ) : (
  123. addrs.map((addr) => (
  124. <Tooltip key={addr} title={addr}>
  125. <span className="address-pill">{addr}</span>
  126. </Tooltip>
  127. ))
  128. )}
  129. </div>
  130. );
  131. },
  132. },
  133. {
  134. title: t('pages.inbounds.traffic'),
  135. key: 'traffic',
  136. align: 'left',
  137. width: 200,
  138. render: (_v, record) => {
  139. const tr = trafficFor(outboundsTraffic, record);
  140. return (
  141. <>
  142. <span className="traffic-up">↑ {SizeFormatter.sizeFormat(tr.up)}</span>
  143. <span className="traffic-sep" />
  144. <span className="traffic-down">↓ {SizeFormatter.sizeFormat(tr.down)}</span>
  145. </>
  146. );
  147. },
  148. },
  149. {
  150. title: t('pages.nodes.latency'),
  151. key: 'testResult',
  152. align: 'left',
  153. width: 140,
  154. render: (_v, _record, index) => {
  155. const r = testResult(outboundTestStates, index);
  156. if (!r) return isTesting(outboundTestStates, index) ? <LoadingOutlined /> : <span className="empty">—</span>;
  157. return <TestResultPopover result={r} />;
  158. },
  159. },
  160. {
  161. title: t('check'),
  162. key: 'test',
  163. align: 'center',
  164. width: 80,
  165. render: (_v, record, index) => (
  166. <Tooltip title={`${t('check')} (${(isUdpOutbound(record) ? 'http' : testMode).toUpperCase()})`}>
  167. <Button
  168. type="primary"
  169. shape="circle"
  170. loading={isTesting(outboundTestStates, index)}
  171. disabled={isUntestable(record) || isTesting(outboundTestStates, index)}
  172. icon={<ThunderboltOutlined />}
  173. aria-label={t('check')}
  174. onClick={() => onTest(index, testMode)}
  175. />
  176. </Tooltip>
  177. ),
  178. },
  179. ],
  180. // eslint-disable-next-line react-hooks/exhaustive-deps
  181. [t, testMode, rows, outboundTestStates, outboundsTraffic],
  182. );
  183. }