FallbacksCard.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { useTranslation } from 'react-i18next';
  2. import { Button, Card, Col, Empty, Input, InputNumber, Row, Select, Space } from 'antd';
  3. import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
  4. import type { FallbackRow } from '@/schemas/forms/inbound-form';
  5. interface FallbacksCardProps {
  6. fallbacks: FallbackRow[];
  7. fallbackChildOptions: { label: string; value: number }[];
  8. addFallback: () => void;
  9. updateFallback: (rowKey: string, patch: Partial<FallbackRow>) => void;
  10. removeFallback: (idx: number) => void;
  11. moveFallback: (idx: number, direction: -1 | 1) => void;
  12. addAllFallbacks: () => void;
  13. }
  14. export default function FallbacksCard({
  15. fallbacks,
  16. fallbackChildOptions,
  17. addFallback,
  18. updateFallback,
  19. removeFallback,
  20. moveFallback,
  21. addAllFallbacks,
  22. }: FallbacksCardProps) {
  23. const { t } = useTranslation();
  24. const addButtons = (
  25. <Space size={8} wrap>
  26. <Button type="primary" ghost size="small" icon={<PlusOutlined />} onClick={addFallback}>
  27. {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
  28. </Button>
  29. <Button
  30. size="small"
  31. onClick={addAllFallbacks}
  32. disabled={fallbackChildOptions.length === 0 || fallbacks.length >= fallbackChildOptions.length}
  33. title={t('pages.inbounds.form.addAllFallbackTooltip')}
  34. >
  35. {t('pages.inbounds.form.addAll')}
  36. </Button>
  37. </Space>
  38. );
  39. return (
  40. <Card
  41. size="small"
  42. className="mt-12"
  43. title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}
  44. extra={addButtons}
  45. >
  46. {fallbacks.length === 0 ? (
  47. <Empty
  48. image={Empty.PRESENTED_IMAGE_SIMPLE}
  49. styles={{ image: { height: 36 } }}
  50. description={t('pages.inbounds.fallbacks.empty') || 'No fallbacks yet'}
  51. style={{ margin: '4px 0 12px' }}
  52. />
  53. ) : (
  54. fallbacks.map((record, idx) => (
  55. <Card
  56. key={record.rowKey}
  57. type="inner"
  58. size="small"
  59. style={{ marginBottom: 8 }}
  60. styles={{ body: { padding: 12 } }}
  61. >
  62. <Space.Compact block style={{ marginBottom: 8 }}>
  63. <Select
  64. aria-label={t('pages.inbounds.fallbacks.pickInbound')}
  65. value={record.childId}
  66. options={fallbackChildOptions}
  67. placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
  68. allowClear
  69. showSearch={{
  70. filterOption: (input, option) =>
  71. ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
  72. }}
  73. style={{ width: '100%' }}
  74. onChange={(v) => updateFallback(record.rowKey, { childId: v ?? null })}
  75. />
  76. <Button
  77. aria-label={t('pages.inbounds.form.moveUp')}
  78. disabled={idx === 0}
  79. onClick={() => moveFallback(idx, -1)}
  80. title={t('pages.inbounds.form.moveUp')}
  81. icon={<ArrowUpOutlined />}
  82. />
  83. <Button
  84. aria-label={t('pages.inbounds.form.moveDown')}
  85. disabled={idx === fallbacks.length - 1}
  86. onClick={() => moveFallback(idx, 1)}
  87. title={t('pages.inbounds.form.moveDown')}
  88. icon={<ArrowDownOutlined />}
  89. />
  90. <Button aria-label={t('delete')} danger onClick={() => removeFallback(idx)} icon={<DeleteOutlined />} />
  91. </Space.Compact>
  92. <Row gutter={[8, 8]}>
  93. <Col xs={24} sm={12}>
  94. <Input
  95. addonBefore="SNI"
  96. placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
  97. value={record.name}
  98. onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })}
  99. />
  100. </Col>
  101. <Col xs={24} sm={12}>
  102. <Input
  103. addonBefore="ALPN"
  104. placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
  105. value={record.alpn}
  106. onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })}
  107. />
  108. </Col>
  109. <Col xs={24} sm={12}>
  110. <Input
  111. addonBefore="Path"
  112. placeholder="/"
  113. value={record.path}
  114. onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
  115. />
  116. </Col>
  117. <Col xs={24} sm={12}>
  118. <Input
  119. addonBefore="Dest"
  120. placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
  121. value={record.dest}
  122. onChange={(e) => updateFallback(record.rowKey, { dest: e.target.value })}
  123. />
  124. </Col>
  125. <Col xs={24} sm={12}>
  126. <InputNumber
  127. addonBefore="xver"
  128. min={0}
  129. max={2}
  130. style={{ width: '100%' }}
  131. value={record.xver}
  132. onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })}
  133. />
  134. </Col>
  135. </Row>
  136. </Card>
  137. ))
  138. )}
  139. </Card>
  140. );
  141. }