useOutboundColumns.tsx 6.4 KB

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