TestResultPopover.tsx 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import type { ReactNode } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Popover } from 'antd';
  4. import { CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
  5. import type { OutboundTestResult } from '@/hooks/useXraySetting';
  6. interface TestResultPopoverProps {
  7. result: OutboundTestResult;
  8. // Custom trigger element; defaults to the ok/fail latency pill.
  9. children?: ReactNode;
  10. }
  11. // Latency pill + detail popover for an outbound test result: per-endpoint
  12. // dial outcomes for TCP probes, HTTP status and the timing breakdown for
  13. // HTTP probes.
  14. export default function TestResultPopover({ result: r, children }: TestResultPopoverProps) {
  15. const { t } = useTranslation();
  16. const breakdown: Array<{ key: string; label: string; value: string }> = [];
  17. if (typeof r.httpStatus === 'number') {
  18. breakdown.push({ key: 'status', label: t('pages.xray.outbound.httpStatus'), value: String(r.httpStatus) });
  19. }
  20. if (typeof r.connectMs === 'number') {
  21. breakdown.push({ key: 'connect', label: t('pages.xray.outbound.breakdownConnect'), value: `${r.connectMs} ms` });
  22. }
  23. if (typeof r.tlsMs === 'number') {
  24. breakdown.push({ key: 'tls', label: t('pages.xray.outbound.breakdownTls'), value: `${r.tlsMs} ms` });
  25. }
  26. if (typeof r.ttfbMs === 'number') {
  27. breakdown.push({ key: 'ttfb', label: t('pages.xray.outbound.breakdownTtfb'), value: `${r.ttfbMs} ms` });
  28. }
  29. return (
  30. <Popover
  31. placement="topLeft"
  32. rootClassName="outbound-test-popover"
  33. content={
  34. <div className="timing-breakdown">
  35. <div className={`td-head ${r.success ? 'ok' : 'fail'}`}>
  36. {r.success ? <span>{r.delay} ms</span> : <span>{r.error || 'failed'}</span>}
  37. {r.mode && <span className="mode-badge">{String(r.mode).toUpperCase()}</span>}
  38. </div>
  39. {(r.endpoints || []).map((ep) => (
  40. <div key={ep.address} className="endpoint-row">
  41. <span className={ep.success ? 'dot-ok' : 'dot-fail'}>●</span>
  42. <span className="ep-addr">{ep.address}</span>
  43. <span className="ep-meta">{ep.success ? `${ep.delay} ms` : ep.error || 'failed'}</span>
  44. </div>
  45. ))}
  46. {breakdown.map((row) => (
  47. <div key={row.key} className="breakdown-row">
  48. <span className="bd-label">{row.label}</span>
  49. <span className="bd-value">{row.value}</span>
  50. </div>
  51. ))}
  52. </div>
  53. }
  54. >
  55. {children ?? (
  56. <span className={r.success ? 'pill-ok' : 'pill-fail'}>
  57. {r.success ? <CheckCircleFilled /> : <CloseCircleFilled />}
  58. {r.success ? <span>{r.delay}&nbsp;ms</span> : <span>failed</span>}
  59. </span>
  60. )}
  61. </Popover>
  62. );
  63. }