index.tsx 18 KB

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