SourceConfigModal.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /*
  2. * <<
  3. * Davinci
  4. * ==
  5. * Copyright (C) 2016 - 2017 EDP
  6. * ==
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * >>
  19. */
  20. import React, { useState, useEffect, useCallback, useMemo } from 'react'
  21. import pick from 'lodash/pick'
  22. import { ISourceFormValues, IDatasourceInfo } from '../types'
  23. import {
  24. Modal,
  25. Form,
  26. Row,
  27. Col,
  28. Button,
  29. Input,
  30. Select,
  31. Icon,
  32. Cascader
  33. } from 'antd'
  34. const FormItem = Form.Item
  35. const TextArea = Input.TextArea
  36. const Option = Select.Option
  37. import { FormComponentProps } from 'antd/lib/form/Form'
  38. import { CascaderOptionType } from 'antd/lib/cascader'
  39. import { SourceProperty } from './types'
  40. import {
  41. EditableFormTable,
  42. EditableColumnProps
  43. } from 'components/Table/Editable'
  44. const utilStyles = require('assets/less/util.less')
  45. interface ISourceConfigModalProps
  46. extends FormComponentProps<ISourceFormValues> {
  47. visible: boolean
  48. formLoading: boolean
  49. testLoading: boolean
  50. source: ISourceFormValues
  51. datasourcesInfo: IDatasourceInfo[]
  52. onSave: (values: any) => void
  53. onClose: () => void
  54. onTestSourceConnection: (
  55. username: string,
  56. password: string,
  57. jdbcUrl: string,
  58. ext: boolean,
  59. version: string
  60. ) => any
  61. onCheckUniqueName: (
  62. pathname: string,
  63. data: any,
  64. resolve: () => any,
  65. reject: (error: string) => any
  66. ) => any
  67. }
  68. const commonFormItemStyle = {
  69. labelCol: { span: 6 },
  70. wrapperCol: { span: 16 }
  71. }
  72. const longFormItemStyle = {
  73. labelCol: { span: 3 },
  74. wrapperCol: { span: 20 }
  75. }
  76. const datasourceInfoDisplayRender = (label: string[]) => label.join(' : ')
  77. const columns: Array<EditableColumnProps<SourceProperty>> = [
  78. {
  79. title: '键',
  80. dataIndex: 'key',
  81. width: '30%',
  82. editable: true,
  83. inputType: 'input'
  84. },
  85. {
  86. title: '值',
  87. dataIndex: 'value',
  88. editable: true,
  89. inputType: 'input'
  90. }
  91. ]
  92. const SourceConfigModal: React.FC<ISourceConfigModalProps> = (props) => {
  93. const {
  94. visible,
  95. source,
  96. datasourcesInfo,
  97. form,
  98. formLoading,
  99. testLoading,
  100. onTestSourceConnection,
  101. onCheckUniqueName,
  102. onSave,
  103. onClose
  104. } = props
  105. if (!source) {
  106. return null
  107. }
  108. const { id: sourceId } = source
  109. const { getFieldDecorator } = form
  110. const [sourceProperties, setSourceProperties] = useState<SourceProperty[]>([])
  111. useEffect(
  112. () => {
  113. let fieldsKeys: Array<keyof ISourceFormValues> = [
  114. 'id',
  115. 'name',
  116. 'type',
  117. 'datasourceInfo',
  118. 'description'
  119. ]
  120. // @FIXME nested object properties name typing
  121. fieldsKeys = []
  122. .concat(fieldsKeys)
  123. .concat(['config.username', 'config.password', 'config.url'])
  124. const fieldsValue = pick(source, fieldsKeys)
  125. form.setFieldsValue(fieldsValue)
  126. },
  127. [source, visible]
  128. )
  129. useEffect(
  130. () => {
  131. setSourceProperties([...(source.config.properties || [])])
  132. },
  133. [source]
  134. )
  135. const addProperty = useCallback(
  136. () => {
  137. const propertySerial = sourceProperties.length + 1
  138. setSourceProperties([
  139. ...sourceProperties,
  140. {
  141. key: `新键 ${propertySerial}`,
  142. value: `新值 ${propertySerial}`
  143. }
  144. ])
  145. },
  146. [sourceProperties]
  147. )
  148. const saveProperties = useCallback((properties: SourceProperty[]) => {
  149. setSourceProperties(properties)
  150. }, [])
  151. const checkNameUnique = useCallback(
  152. (_, name = '', callback) => {
  153. const { id, projectId } = source
  154. const data = { id, name, projectId }
  155. if (!name) {
  156. callback()
  157. }
  158. onCheckUniqueName(
  159. 'source',
  160. data,
  161. () => {
  162. callback()
  163. },
  164. (err) => callback(err)
  165. )
  166. },
  167. [onCheckUniqueName, source]
  168. )
  169. const datasourceInfoChange = useCallback(
  170. (value: string[]) => {
  171. const datasourceName = value[0]
  172. const selectedDatasource = datasourcesInfo.find(
  173. ({ name }) => name === datasourceName
  174. )
  175. const prefix = selectedDatasource.prefix
  176. const currentUrl = form.getFieldValue('config.url') as string
  177. let hasMatched = false
  178. let newUrl = currentUrl.replace(/^jdbc:([\w:]+):/, (match) => {
  179. hasMatched = !!match
  180. return prefix
  181. })
  182. if (!hasMatched) {
  183. newUrl = prefix + currentUrl
  184. }
  185. form.setFieldsValue({ 'config.url': newUrl })
  186. },
  187. [datasourcesInfo]
  188. )
  189. const testSourceConnection = useCallback(
  190. () => {
  191. const {
  192. datasourceInfo,
  193. config
  194. } = form.getFieldsValue() as ISourceFormValues
  195. const { username, password, url } = config
  196. const version =
  197. datasourceInfo[1] === 'Default' ? '' : datasourceInfo[1] || ''
  198. onTestSourceConnection(username, password, url, !!version, version)
  199. },
  200. [form, onTestSourceConnection]
  201. )
  202. const save = useCallback(
  203. () => {
  204. form.validateFieldsAndScroll((err, values) => {
  205. if (!err) {
  206. values.config.properties = sourceProperties.filter(({ key }) => !!key)
  207. onSave(values)
  208. }
  209. })
  210. },
  211. [form, sourceProperties, onSave]
  212. )
  213. const reset = useCallback(
  214. () => {
  215. form.resetFields()
  216. setSourceProperties([])
  217. },
  218. [form]
  219. )
  220. const cascaderOptions: CascaderOptionType[] = useMemo(
  221. () =>
  222. datasourcesInfo.map(({ name, versions }) => ({
  223. label: name,
  224. value: name,
  225. ...(versions && {
  226. children: versions.map((ver) => ({
  227. label: ver,
  228. value: ver
  229. }))
  230. })
  231. })),
  232. [datasourcesInfo]
  233. )
  234. const modalButtons = useMemo(
  235. () => [
  236. <Button
  237. key="submit"
  238. size="large"
  239. type="primary"
  240. loading={formLoading}
  241. disabled={formLoading}
  242. onClick={save}
  243. >
  244. 保 存
  245. </Button>,
  246. <Button key="back" size="large" onClick={onClose}>
  247. 取 消
  248. </Button>
  249. ],
  250. [form, formLoading, sourceProperties, onSave, onClose]
  251. )
  252. return (
  253. <Modal
  254. title={`${!sourceId ? '新增' : '修改'}`}
  255. wrapClassName="ant-modal-large"
  256. maskClosable={false}
  257. visible={visible}
  258. footer={modalButtons}
  259. onCancel={onClose}
  260. afterClose={reset}
  261. >
  262. <Form>
  263. <Row>
  264. <FormItem className={utilStyles.hide}>
  265. {getFieldDecorator<ISourceFormValues>('id')(<Input />)}
  266. </FormItem>
  267. <Col span={12}>
  268. <FormItem label="名称" {...commonFormItemStyle} hasFeedback>
  269. {getFieldDecorator<ISourceFormValues>('name', {
  270. rules: [
  271. {
  272. required: true,
  273. message: '名称不能为空'
  274. },
  275. {
  276. validator: checkNameUnique
  277. }
  278. ]
  279. })(<Input autoComplete="off" placeholder="名称" />)}
  280. </FormItem>
  281. </Col>
  282. </Row>
  283. <Row>
  284. <Col span={12}>
  285. <FormItem label="类型" {...commonFormItemStyle}>
  286. {getFieldDecorator<ISourceFormValues>('type', {
  287. initialValue: 'jdbc'
  288. })(
  289. <Select>
  290. <Option value="jdbc">JDBC</Option>
  291. <Option value="csv">CSV文件</Option>
  292. </Select>
  293. )}
  294. </FormItem>
  295. </Col>
  296. <Col span={12}>
  297. <FormItem label="数据库" {...commonFormItemStyle}>
  298. {getFieldDecorator<ISourceFormValues>('datasourceInfo', {
  299. initialValue: []
  300. })(
  301. <Cascader
  302. options={cascaderOptions}
  303. displayRender={datasourceInfoDisplayRender}
  304. onChange={datasourceInfoChange}
  305. />
  306. )}
  307. </FormItem>
  308. </Col>
  309. <Col span={12}>
  310. <FormItem label="用户名" {...commonFormItemStyle}>
  311. {getFieldDecorator('config.username', {
  312. initialValue: ''
  313. })(<Input autoComplete="off" placeholder="用户名" />)}
  314. </FormItem>
  315. </Col>
  316. <Col span={12}>
  317. <FormItem label="密码" {...commonFormItemStyle}>
  318. {getFieldDecorator('config.password', {
  319. initialValue: ''
  320. })(
  321. <Input
  322. autoComplete="off"
  323. placeholder="密码"
  324. type="password"
  325. />
  326. )}
  327. </FormItem>
  328. </Col>
  329. </Row>
  330. <FormItem label="连接Url" {...longFormItemStyle}>
  331. {getFieldDecorator('config.url', {
  332. rules: [
  333. {
  334. required: true,
  335. message: 'Url 不能为空'
  336. }
  337. ],
  338. initialValue: ''
  339. })(
  340. <Input
  341. placeholder="连接Url"
  342. autoComplete="off"
  343. addonAfter={
  344. testLoading ? (
  345. <Icon type="loading" />
  346. ) : (
  347. <span
  348. onClick={testSourceConnection}
  349. style={{ cursor: 'pointer' }}
  350. >
  351. 点击测试
  352. </span>
  353. )
  354. }
  355. />
  356. )}
  357. </FormItem>
  358. <FormItem label="描述" {...longFormItemStyle}>
  359. {getFieldDecorator('description', {
  360. initialValue: ''
  361. })(
  362. <TextArea
  363. placeholder="描述"
  364. autosize={{ minRows: 2, maxRows: 6 }}
  365. />
  366. )}
  367. </FormItem>
  368. </Form>
  369. <FormItem label="配置信息" {...longFormItemStyle}>
  370. <Button
  371. type="primary"
  372. size="small"
  373. shape="circle"
  374. icon="plus"
  375. onClick={addProperty}
  376. />
  377. <EditableFormTable
  378. data={sourceProperties}
  379. dataKey="key"
  380. columns={columns}
  381. onSave={saveProperties}
  382. />
  383. </FormItem>
  384. </Modal>
  385. )
  386. }
  387. export default Form.create<ISourceConfigModalProps>()(SourceConfigModal)