index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  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 from 'react'
  21. import Helmet from 'react-helmet'
  22. import { connect } from 'react-redux'
  23. import { createStructuredSelector } from 'reselect'
  24. import memoizeOne from 'memoize-one'
  25. import { Link } from 'react-router-dom'
  26. import { RouteComponentWithParams } from 'utils/types'
  27. import { compose, Dispatch } from 'redux'
  28. import injectReducer from 'utils/injectReducer'
  29. import injectSaga from 'utils/injectSaga'
  30. import reducer from './reducer'
  31. import saga from './sagas'
  32. import Container, { ContainerTitle, ContainerBody } from 'components/Container'
  33. import Box from 'components/Box'
  34. import SearchFilterDropdown from 'components/SearchFilterDropdown'
  35. import SourceConfigModal from './components/SourceConfigModal'
  36. import UploadCsvModal from './components/UploadCsvModal'
  37. import ResetConnectionModal from './components/ResetConnectionModal'
  38. import {
  39. message,
  40. Row,
  41. Col,
  42. Table,
  43. Button,
  44. Tooltip,
  45. Icon,
  46. Popconfirm,
  47. Breadcrumb
  48. } from 'antd'
  49. import { ButtonProps } from 'antd/lib/button/button'
  50. import { ColumnProps, PaginationConfig, SorterResult } from 'antd/lib/table'
  51. import { SourceActions, SourceActionType } from './actions'
  52. import {
  53. makeSelectSources,
  54. makeSelectListLoading,
  55. makeSelectFormLoading,
  56. makeSelectTestLoading,
  57. makeSelectResetLoading,
  58. makeSelectDatasourcesInfo
  59. } from './selectors'
  60. import { checkNameUniqueAction } from '../App/actions'
  61. import { makeSelectCurrentProject } from '../Projects/selectors'
  62. import ModulePermission from '../Account/components/checkModulePermission'
  63. import { initializePermission } from '../Account/components/checkUtilPermission'
  64. import { IProject } from 'containers/Projects/types'
  65. import {
  66. ISourceBase,
  67. ISource,
  68. ICSVMetaInfo,
  69. ISourceFormValues,
  70. SourceResetConnectionProperties
  71. } from './types'
  72. import utilStyles from 'assets/less/util.less'
  73. type ISourceListProps = ReturnType<typeof mapStateToProps> &
  74. ReturnType<typeof mapDispatchToProps> &
  75. RouteComponentWithParams
  76. interface ISourceListStates {
  77. screenWidth: number
  78. tempFilterSourceName: string
  79. filterSourceName: string
  80. filterDropdownVisible: boolean
  81. tableSorter: SorterResult<ISource>
  82. sourceModalVisible: boolean
  83. uploadModalVisible: boolean
  84. csvUploading: boolean
  85. resetModalVisible: boolean
  86. resetSource: ISource
  87. editingSource: ISourceFormValues
  88. csvSourceId: number
  89. }
  90. const emptySource: ISourceFormValues = {
  91. id: 0,
  92. name: '',
  93. type: 'jdbc',
  94. description: '',
  95. projectId: 0,
  96. datasourceInfo: [],
  97. config: {
  98. username: '',
  99. password: '',
  100. url: '',
  101. properties: []
  102. }
  103. }
  104. export class SourceList extends React.PureComponent<ISourceListProps,
  105. ISourceListStates> {
  106. public state: Readonly<ISourceListStates> = {
  107. screenWidth: document.documentElement.clientWidth,
  108. tempFilterSourceName: '',
  109. tableSorter: null,
  110. filterSourceName: '',
  111. filterDropdownVisible: false,
  112. sourceModalVisible: false,
  113. uploadModalVisible: false,
  114. csvUploading: false,
  115. resetModalVisible: false,
  116. resetSource: null,
  117. editingSource: { ...emptySource },
  118. csvSourceId: null
  119. }
  120. private basePagination: PaginationConfig = {
  121. defaultPageSize: 20,
  122. showSizeChanger: true
  123. }
  124. public componentWillMount() {
  125. const { onLoadSources, onLoadDatasourcesInfo, match } = this.props
  126. const projectId = +match.params.projectId
  127. onLoadSources(projectId)
  128. onLoadDatasourcesInfo()
  129. window.addEventListener('resize', this.setScreenWidth, false)
  130. }
  131. public componentWillUnmount() {
  132. window.removeEventListener('resize', this.setScreenWidth, false)
  133. }
  134. private setScreenWidth = () => {
  135. this.setState({ screenWidth: document.documentElement.clientWidth })
  136. }
  137. private getFilterSources = memoizeOne(
  138. (sourceName: string, sources: ISourceBase[]) => {
  139. if (!Array.isArray(sources) || !sources.length) {
  140. return []
  141. }
  142. const regex = new RegExp(sourceName, 'gi')
  143. const filterSources = sources.filter(
  144. (v) => v.name.match(regex) || v.description.match(regex)
  145. )
  146. return filterSources
  147. }
  148. )
  149. private static getSourcePermission = memoizeOne((project: IProject) => ({
  150. sourcePermission: initializePermission(project, 'sourcePermission'),
  151. AdminButton: ModulePermission<ButtonProps>(project, 'source', true)(Button),
  152. EditButton: ModulePermission<ButtonProps>(project, 'source', false)(Button)
  153. }))
  154. private getTableColumns = ({
  155. sourcePermission,
  156. AdminButton,
  157. EditButton
  158. }: ReturnType<typeof SourceList.getSourcePermission>) => {
  159. const {
  160. tempFilterSourceName,
  161. filterSourceName,
  162. filterDropdownVisible,
  163. tableSorter
  164. } = this.state
  165. const { resetLoading } = this.props
  166. const columns: Array<ColumnProps<ISource>> = [
  167. {
  168. title: '名称',
  169. dataIndex: 'name',
  170. filterDropdown: (
  171. <SearchFilterDropdown
  172. placeholder='名称'
  173. value={tempFilterSourceName}
  174. onChange={this.filterSourceNameChange}
  175. onSearch={this.searchSource}
  176. />
  177. ),
  178. filterDropdownVisible,
  179. onFilterDropdownVisibleChange: (visible: boolean) =>
  180. this.setState({
  181. filterDropdownVisible: visible
  182. }),
  183. sorter: (a, b) => (a.name > b.name ? -1 : 1),
  184. sortOrder:
  185. tableSorter && tableSorter.columnKey === 'name'
  186. ? tableSorter.order
  187. : void 0
  188. },
  189. {
  190. title: '描述',
  191. dataIndex: 'description'
  192. },
  193. {
  194. title: '类型',
  195. dataIndex: 'type',
  196. filters: [
  197. {
  198. text: 'JDBC',
  199. value: 'jdbc'
  200. },
  201. {
  202. text: 'CSV',
  203. value: 'csv'
  204. }
  205. ],
  206. filterMultiple: false,
  207. onFilter: (val, record) => record.type === val,
  208. render: (_, record) => {
  209. const type = record.type
  210. return type && type.toUpperCase()
  211. }
  212. }
  213. ]
  214. if (filterSourceName) {
  215. const regex = new RegExp(`(${filterSourceName})`, 'gi')
  216. columns[0].render = (text: string) => (
  217. <span
  218. dangerouslySetInnerHTML={{
  219. __html: text.replace(
  220. regex,
  221. `<span class='${utilStyles.highlight}'>$1</span>`
  222. )
  223. }}
  224. />
  225. )
  226. }
  227. if (sourcePermission) {
  228. columns.push({
  229. title: '操作',
  230. key: 'action',
  231. width: 180,
  232. render: (_, record) => (
  233. <span className='ant-table-action-column'>
  234. <Tooltip title='重置连接'>
  235. <EditButton
  236. icon='reload'
  237. shape='circle'
  238. type='ghost'
  239. disabled={resetLoading}
  240. onClick={this.openResetSource(record)}
  241. />
  242. </Tooltip>
  243. <Tooltip title='修改'>
  244. <EditButton
  245. icon='edit'
  246. shape='circle'
  247. type='ghost'
  248. onClick={this.editSource(record.id)}
  249. />
  250. </Tooltip>
  251. <Popconfirm
  252. title='确定删除?'
  253. placement='bottom'
  254. onConfirm={this.deleteSource(record.id)}
  255. >
  256. <Tooltip title='删除'>
  257. <AdminButton icon='delete' shape='circle' type='ghost' />
  258. </Tooltip>
  259. </Popconfirm>
  260. {record && record.type === 'csv' ? (
  261. <Tooltip title='上传'>
  262. <EditButton
  263. icon='upload'
  264. shape='circle'
  265. type='ghost'
  266. onClick={this.showUploadModal(record.id)}
  267. />
  268. </Tooltip>
  269. ) : (
  270. ''
  271. )}
  272. </span>
  273. )
  274. })
  275. }
  276. return columns
  277. }
  278. private addSource = () => {
  279. this.setState({
  280. editingSource: {
  281. ...emptySource,
  282. projectId: +this.props.match.params.projectId
  283. },
  284. sourceModalVisible: true
  285. })
  286. }
  287. private openResetSource = (source: ISource) => () => {
  288. this.setState({
  289. resetModalVisible: true,
  290. resetSource: source
  291. })
  292. }
  293. private resetConnection = (properties: SourceResetConnectionProperties) => {
  294. this.props.onResetSourceConnection(properties, () => {
  295. this.closeResetConnectionModal()
  296. })
  297. }
  298. private closeResetConnectionModal = () => {
  299. this.setState({ resetModalVisible: false })
  300. }
  301. private editSource = (sourceId: number) => () => {
  302. this.props.onLoadSourceDetail(sourceId, (editingSource) => {
  303. this.setState({
  304. editingSource: {
  305. ...editingSource,
  306. datasourceInfo: this.getDatasourceInfo(editingSource)
  307. },
  308. sourceModalVisible: true
  309. })
  310. })
  311. }
  312. private getDatasourceInfo = (source: ISource): string[] => {
  313. const { datasourcesInfo } = this.props
  314. const { url, version } = source.config
  315. const matchResult = url.match(/^jdbc\:(\w+)\:/)
  316. if (matchResult) {
  317. const datasource = datasourcesInfo.find(
  318. (info) => info.name === matchResult[1]
  319. )
  320. return datasource
  321. ? datasource.versions.length
  322. ? [datasource.name, version || 'Default']
  323. : [datasource.name]
  324. : []
  325. } else {
  326. return []
  327. }
  328. }
  329. private deleteSource = (sourceId: number) => () => {
  330. const { onDeleteSource } = this.props
  331. onDeleteSource(sourceId)
  332. }
  333. private showUploadModal = (sourceId: number) => () => {
  334. this.setState({
  335. csvSourceId: sourceId,
  336. uploadModalVisible: true
  337. })
  338. }
  339. private saveSourceForm = (values: ISourceFormValues) => {
  340. const { match } = this.props
  341. const { datasourceInfo, config, ...rest } = values
  342. const version =
  343. datasourceInfo[1] === 'Default' ? '' : datasourceInfo[1] || ''
  344. const requestValue = {
  345. ...rest,
  346. config: {
  347. ...config,
  348. ext: !!version,
  349. version
  350. },
  351. projectId: Number(match.params.projectId)
  352. }
  353. if (!values.id) {
  354. this.props.onAddSource({ ...requestValue }, () => {
  355. this.closeSourceForm()
  356. })
  357. } else {
  358. this.props.onEditSource({ ...requestValue }, () => {
  359. this.closeSourceForm()
  360. })
  361. }
  362. }
  363. private closeSourceForm = () => {
  364. this.setState({
  365. sourceModalVisible: false
  366. })
  367. }
  368. private closeUploadModal = () => {
  369. this.setState({
  370. uploadModalVisible: false
  371. })
  372. }
  373. private uploadCsv = (csvMetaInfo: ICSVMetaInfo) => {
  374. this.setState({ csvUploading: true }, () => {
  375. this.props.onUploadCsvFile(
  376. csvMetaInfo,
  377. () => {
  378. this.closeUploadModal()
  379. message.info('csv 文件上传成功!')
  380. this.setState({ csvUploading: false })
  381. },
  382. () => {
  383. this.setState({ csvUploading: false })
  384. }
  385. )
  386. })
  387. }
  388. private tableChange = (_1, _2, sorter: SorterResult<ISource>) => {
  389. this.setState({
  390. tableSorter: sorter
  391. })
  392. }
  393. private filterSourceNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  394. this.setState({
  395. tempFilterSourceName: e.target.value,
  396. filterSourceName: ''
  397. })
  398. }
  399. private searchSource = (value: string) => {
  400. this.setState({
  401. filterSourceName: value,
  402. filterDropdownVisible: false
  403. })
  404. }
  405. private testSourceConnection = (
  406. username,
  407. password,
  408. jdbcUrl,
  409. ext,
  410. version
  411. ) => {
  412. if (jdbcUrl) {
  413. this.props.onTestSourceConnection({
  414. username,
  415. password,
  416. url: jdbcUrl,
  417. ext,
  418. version
  419. })
  420. } else {
  421. message.error('连接 Url 都不能为空')
  422. }
  423. }
  424. public render() {
  425. const {
  426. filterSourceName,
  427. sourceModalVisible,
  428. uploadModalVisible,
  429. resetModalVisible,
  430. resetSource,
  431. csvUploading,
  432. csvSourceId,
  433. screenWidth,
  434. editingSource
  435. } = this.state
  436. const {
  437. sources,
  438. listLoading,
  439. formLoading,
  440. testLoading,
  441. currentProject,
  442. datasourcesInfo,
  443. onValidateCsvTableName,
  444. onCheckUniqueName
  445. } = this.props
  446. const {
  447. sourcePermission,
  448. AdminButton,
  449. EditButton
  450. } = SourceList.getSourcePermission(currentProject)
  451. const tableColumns = this.getTableColumns({
  452. sourcePermission,
  453. AdminButton,
  454. EditButton
  455. })
  456. const tablePagination: PaginationConfig = {
  457. ...this.basePagination,
  458. simple: screenWidth <= 768
  459. }
  460. const filterSources = this.getFilterSources(filterSourceName, sources)
  461. const pathname = this.props.history.location.pathname
  462. return (
  463. <Container>
  464. <Helmet title='数据源' />
  465. {
  466. !pathname.includes('dataManager') &&
  467. <ContainerTitle>
  468. <Row>
  469. <Col span={24} className={utilStyles.shortcut}>
  470. <Breadcrumb className={utilStyles.breadcrumb}>
  471. <Breadcrumb.Item>
  472. <Link to=''>Source</Link>
  473. </Breadcrumb.Item>
  474. </Breadcrumb>
  475. <Link to={`/account/organization/${currentProject.orgId}`}>
  476. <i className='iconfont icon-organization' />
  477. </Link>
  478. </Col>
  479. </Row>
  480. </ContainerTitle>}
  481. <ContainerBody>
  482. <Box>
  483. <Box.Header>
  484. <Box.Title>
  485. <Icon type='bars' />
  486. 数据源列表
  487. </Box.Title>
  488. <Box.Tools>
  489. <Tooltip placement='bottom' title='新增'>
  490. <AdminButton
  491. type='primary'
  492. icon='plus'
  493. onClick={this.addSource}
  494. />
  495. </Tooltip>
  496. </Box.Tools>
  497. </Box.Header>
  498. <Box.Body>
  499. <Row>
  500. <Col span={24}>
  501. <Table
  502. bordered
  503. rowKey='id'
  504. loading={listLoading}
  505. dataSource={filterSources}
  506. columns={tableColumns}
  507. pagination={tablePagination}
  508. onChange={this.tableChange}
  509. />
  510. </Col>
  511. </Row>
  512. <SourceConfigModal
  513. source={editingSource}
  514. datasourcesInfo={datasourcesInfo}
  515. visible={sourceModalVisible}
  516. formLoading={formLoading}
  517. testLoading={testLoading}
  518. onSave={this.saveSourceForm}
  519. onClose={this.closeSourceForm}
  520. onTestSourceConnection={this.testSourceConnection}
  521. onCheckUniqueName={onCheckUniqueName}
  522. />
  523. <UploadCsvModal
  524. visible={uploadModalVisible}
  525. uploading={csvUploading}
  526. sourceId={csvSourceId}
  527. onValidate={onValidateCsvTableName}
  528. onCancel={this.closeUploadModal}
  529. onOk={this.uploadCsv}
  530. />
  531. <ResetConnectionModal
  532. visible={resetModalVisible}
  533. source={resetSource}
  534. onConfirm={this.resetConnection}
  535. onCancel={this.closeResetConnectionModal}
  536. />
  537. </Box.Body>
  538. </Box>
  539. </ContainerBody>
  540. </Container>
  541. )
  542. }
  543. }
  544. const mapDispatchToProps = (dispatch: Dispatch<SourceActionType>) => ({
  545. onLoadSources: (projectId: number) =>
  546. dispatch(SourceActions.loadSources(projectId)),
  547. onLoadSourceDetail: (sourceId: number, resolve: (source: ISource) => void) =>
  548. dispatch(SourceActions.loadSourceDetail(sourceId, resolve)),
  549. onAddSource: (source: ISource, resolve: () => any) =>
  550. dispatch(SourceActions.addSource(source, resolve)),
  551. onDeleteSource: (id: number) => dispatch(SourceActions.deleteSource(id)),
  552. onEditSource: (source: ISource, resolve: () => void) =>
  553. dispatch(SourceActions.editSource(source, resolve)),
  554. onTestSourceConnection: (testSource: Omit<ISource['config'], 'properties'>) =>
  555. dispatch(SourceActions.testSourceConnection(testSource)),
  556. onResetSourceConnection: (
  557. properties: SourceResetConnectionProperties,
  558. resolve: () => void
  559. ) => dispatch(SourceActions.resetSourceConnection(properties, resolve)),
  560. onValidateCsvTableName: (
  561. csvMeta: ICSVMetaInfo,
  562. callback: (errMsg?: string) => void
  563. ) => dispatch(SourceActions.validateCsvTableName(csvMeta, callback)),
  564. onUploadCsvFile: (
  565. csvMeta: ICSVMetaInfo,
  566. resolve: () => void,
  567. reject: () => void
  568. ) => dispatch(SourceActions.uploadCsvFile(csvMeta, resolve, reject)),
  569. onCheckUniqueName: (
  570. pathname: string,
  571. data: any,
  572. resolve: () => void,
  573. reject: (err: string) => void
  574. ) => dispatch(checkNameUniqueAction(pathname, data, resolve, reject)),
  575. onLoadDatasourcesInfo: () => dispatch(SourceActions.loadDatasourcesInfo())
  576. })
  577. const mapStateToProps = createStructuredSelector({
  578. sources: makeSelectSources(),
  579. listLoading: makeSelectListLoading(),
  580. formLoading: makeSelectFormLoading(),
  581. testLoading: makeSelectTestLoading(),
  582. resetLoading: makeSelectResetLoading(),
  583. currentProject: makeSelectCurrentProject(),
  584. datasourcesInfo: makeSelectDatasourcesInfo()
  585. })
  586. const withConnect = connect(mapStateToProps, mapDispatchToProps)
  587. const withReducer = injectReducer({ key: 'source', reducer })
  588. const withSaga = injectSaga({ key: 'source', saga })
  589. export default compose(withReducer, withSaga, withConnect)(SourceList)