github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/ui/src/components/CreateOrUpdateTask/MigrateRule.tsx (about) 1 import React, { useEffect, useMemo, useState } from 'react' 2 import { useTranslation } from 'react-i18next' 3 4 import { 5 Form, 6 Button, 7 Card, 8 Select, 9 Input, 10 Cascader, 11 message, 12 Space, 13 Checkbox, 14 } from '~/uikit' 15 import { 16 FileAddOutlined, 17 DoubleRightOutlined, 18 CloseOutlined, 19 DatabaseOutlined, 20 LoadingOutlined, 21 } from '~/uikit/icons' 22 import { StepCompnent } from '~/components/CreateOrUpdateTask/shared' 23 import { 24 useDmapiGetSourceSchemaListQuery, 25 useDmapiGetSourceTableListMutation, 26 } from '~/models/source' 27 28 const itemLayout = { 29 labelCol: { span: 6 }, 30 wrapperCol: { span: 18 }, 31 } 32 33 const createPattern = (name: string[]) => { 34 if (name.length === 0) return '*' 35 if (name.length === 1) return name[0] 36 return '~' + name.map(i => `^${i}$`).join('|') 37 } 38 39 const MigrateRule: StepCompnent = ({ prev, initialValues }) => { 40 const [t] = useTranslation() 41 const [currentSource, setCurrentSource] = useState('') 42 const [cascaderValue, setCascaderValue] = useState<string[][]>([]) 43 const [form] = Form.useForm() 44 const [getSourceTable] = useDmapiGetSourceTableListMutation() 45 const { data: schemas, isFetching } = useDmapiGetSourceSchemaListQuery( 46 { sourceName: currentSource }, 47 { skip: !currentSource } 48 ) 49 const selectedSource = useMemo(() => { 50 return ( 51 initialValues?.source_config?.source_conf?.map(i => i.source_name) ?? [] 52 ) 53 }, [initialValues]) 54 55 const [cascaderOptions, setCascaderOptions] = useState( 56 schemas?.map(item => ({ 57 value: item, 58 label: item, 59 isLeaf: false, 60 })) ?? [] 61 ) 62 63 const loadCascaderData = async (selectedOptions: any) => { 64 const targetOption = selectedOptions[selectedOptions.length - 1] 65 targetOption.loading = true 66 const res = await getSourceTable({ 67 sourceName: currentSource, 68 schemaName: targetOption.value, 69 }).unwrap() 70 targetOption.loading = false 71 targetOption.children = res.map(item => ({ 72 value: item, 73 label: item, 74 isLeaf: true, 75 })) 76 setCascaderOptions([...cascaderOptions]) 77 } 78 79 const handelSchemaSelectInCascader = (value: string[][], field: any) => { 80 setCascaderValue(value) 81 const dbPattern = createPattern([ 82 ...new Set(value.map((item: string[]) => item[0])), 83 ] as string[]) 84 const tablePattern = createPattern([ 85 ...new Set(value.map((item: string[]) => item[1]).filter(Boolean)), 86 ] as string[]) 87 const newData = [...form.getFieldValue('table_migrate_rule')] 88 newData[field.key] = { 89 ...newData[field.key], 90 source: { 91 ...newData[field.key].source, 92 schema: dbPattern, 93 table: tablePattern, 94 }, 95 } 96 form.setFieldsValue({ 97 table_migrate_rule: newData, 98 }) 99 } 100 101 const handleSubmit = (startAfterSaved: boolean) => { 102 const rules = form.getFieldValue('table_migrate_rule') 103 if (rules.length === 0) { 104 message.error({ 105 content: t('migrate rules is required'), 106 }) 107 return 108 } 109 if (startAfterSaved) { 110 form.setFieldsValue({ 111 ...form.getFieldsValue(), 112 start_after_saved: true, 113 }) 114 } 115 form.submit() 116 } 117 118 useEffect(() => { 119 if (schemas) { 120 setCascaderOptions( 121 schemas?.map(item => ({ 122 value: item, 123 label: item, 124 isLeaf: false, 125 })) 126 ) 127 128 setCascaderValue([]) 129 } 130 }, [schemas]) 131 132 return ( 133 <Form initialValues={initialValues} name="migrateRule" form={form}> 134 <div className="grid grid-cols-1 gap-4 auto-rows-fr my-4"> 135 <Form.List name="table_migrate_rule"> 136 {(fields, { add, remove }) => ( 137 <> 138 {fields.map(field => ( 139 <Card 140 hoverable 141 key={field.key} 142 className="min-h-200px relative !border-[#d9d9d9] group" 143 > 144 <CloseOutlined 145 onClick={() => remove(field.name)} 146 className="!text-gray-500 absolute top-2 right-2 group-hover:opacity-100 opacity-0 transition" 147 /> 148 <div className="grid grid-cols-26"> 149 <div className="col-span-8"> 150 <h1 className="font-bold">{t('source')}</h1> 151 <Form.Item 152 label={t('source')} 153 name={[field.name, 'source', 'source_name']} 154 {...itemLayout} 155 > 156 <Select 157 onChange={val => setCurrentSource(val as string)} 158 > 159 {selectedSource.map(s => ( 160 <Select.Option key={s} value={s}> 161 {s} 162 </Select.Option> 163 ))} 164 </Select> 165 </Form.Item> 166 <Form.Item 167 label={t('database')} 168 tooltip={t( 169 'create task migrate rule source schema tooltip' 170 )} 171 name={[field.name, 'source', 'schema']} 172 {...itemLayout} 173 > 174 <Input 175 placeholder="*" 176 addonAfter={ 177 <Cascader 178 options={cascaderOptions} 179 loadData={loadCascaderData} 180 multiple 181 maxTagCount="responsive" 182 value={cascaderValue} 183 onChange={(val: any) => 184 handelSchemaSelectInCascader(val, field) 185 } 186 > 187 {isFetching ? ( 188 <LoadingOutlined /> 189 ) : ( 190 <DatabaseOutlined /> 191 )} 192 </Cascader> 193 } 194 /> 195 </Form.Item> 196 <Form.Item 197 label={t('table')} 198 tooltip={t( 199 'create task migrate rule source table tooltip' 200 )} 201 name={[field.name, 'source', 'table']} 202 {...itemLayout} 203 > 204 <Input placeholder="tbl_*" /> 205 </Form.Item> 206 </div> 207 208 <div className="flex items-center justify-center col-span-1"> 209 <DoubleRightOutlined /> 210 </div> 211 212 <div className="col-span-8"> 213 <h1 className="font-bold">{t('event filter')}</h1> 214 <Form.Item 215 label={t('event filter')} 216 {...itemLayout} 217 name={[field.name, 'binlog_filter_rule']} 218 tooltip={t( 219 'create task migrate rule binlog_filter_rule tooltip' 220 )} 221 > 222 <Select 223 mode="multiple" 224 allowClear 225 placeholder="Please select filter" 226 > 227 {initialValues?.binlog_filter_rule_array?.map( 228 rule => ( 229 <Select.Option key={rule.name} value={rule.name}> 230 {rule.name} 231 </Select.Option> 232 ) 233 )} 234 </Select> 235 </Form.Item> 236 </div> 237 238 <div className="flex items-center justify-center col-span-1"> 239 <DoubleRightOutlined /> 240 </div> 241 242 <div className="col-span-8"> 243 <h1 className="font-bold">{t('target')}</h1> 244 <Form.Item label={t('target')} {...itemLayout}> 245 <Input 246 value={initialValues?.target_config.host} 247 disabled 248 /> 249 </Form.Item> 250 <Form.Item 251 label={t('database')} 252 tooltip={t( 253 'create task migrate rule target schema tooltip' 254 )} 255 name={[field.name, 'target', 'schema']} 256 {...itemLayout} 257 > 258 <Input /> 259 </Form.Item> 260 <Form.Item 261 label={t('table')} 262 tooltip={t( 263 'create task migrate rule target table tooltip' 264 )} 265 name={[field.name, 'target', 'table']} 266 {...itemLayout} 267 > 268 <Input /> 269 </Form.Item> 270 </div> 271 </div> 272 </Card> 273 ))} 274 275 <Button 276 className="!h-200px" 277 type="dashed" 278 icon={<FileAddOutlined />} 279 onClick={() => 280 add({ 281 source: { schema: '', table: '' }, 282 binlog_filter_rule: [], 283 }) 284 } 285 > 286 {t('add migrate rule')} 287 </Button> 288 </> 289 )} 290 </Form.List> 291 <Form.Item name="start_after_saved" hidden> 292 <Checkbox /> 293 </Form.Item> 294 </div> 295 296 <Form.Item> 297 <Space> 298 <Button onClick={prev}>{t('previous')}</Button> 299 <Button type="primary" onClick={() => handleSubmit(false)}> 300 {t('save')} 301 </Button> 302 <Button type="primary" onClick={() => handleSubmit(true)}> 303 {t('save and run')} 304 </Button> 305 </Space> 306 </Form.Item> 307 </Form> 308 ) 309 } 310 311 export default MigrateRule