index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  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 { compose, Dispatch } from 'redux'
  22. import { connect } from 'react-redux'
  23. import { createStructuredSelector } from 'reselect'
  24. import memoizeOne from 'memoize-one'
  25. import Helmet from 'react-helmet'
  26. import { Link } from 'react-router-dom'
  27. import { RouteComponentWithParams } from 'utils/types'
  28. import injectReducer from 'utils/injectReducer'
  29. import injectSaga from 'utils/injectSaga'
  30. import reducer from './reducer'
  31. import sagas from './sagas'
  32. import { checkNameUniqueAction } from 'containers/App/actions'
  33. import { ViewActions, ViewActionType } from './actions'
  34. import { makeSelectLoading, makeSelectViews } from './selectors'
  35. import { makeSelectCurrentProject } from 'containers/Projects/selectors'
  36. import ModulePermission from '../Account/components/checkModulePermission'
  37. import { initializePermission } from '../Account/components/checkUtilPermission'
  38. import {
  39. Breadcrumb,
  40. Button,
  41. Col,
  42. Dropdown,
  43. Icon,
  44. Menu,
  45. message,
  46. Popconfirm,
  47. Row,
  48. Spin,
  49. Table,
  50. Tooltip,
  51. Tree
  52. } from 'antd'
  53. import { ColumnProps, PaginationConfig, SorterResult } from 'antd/lib/table'
  54. import { ButtonProps } from 'antd/lib/button'
  55. import Container, { ContainerBody, ContainerTitle } from 'components/Container'
  56. import Box from 'components/Box'
  57. import SearchFilterDropdown from 'components/SearchFilterDropdown'
  58. import CopyModal from './components/CopyModal'
  59. import CatalogueModal from './components/CatalogueModal'
  60. import { ICatalogue, IViewBase, IViewLoading } from './types'
  61. import { IProject } from '../Projects/types'
  62. import utilStyles from 'assets/less/util.less'
  63. import styles from './index.less'
  64. import request from 'utils/request'
  65. import api from 'utils/api'
  66. import classnames from 'classnames'
  67. interface IViewListStateProps {
  68. views: IViewBase[]
  69. currentProject: IProject
  70. loading: IViewLoading
  71. }
  72. interface IViewListDispatchProps {
  73. onLoadViews: (projectId: number, parentId: number) => void
  74. onDeleteView: (viewId: number, resolve: () => void) => void
  75. onCopyView: (view: IViewBase, resolve: () => void) => void
  76. onCheckName: (data, resolve, reject) => void
  77. }
  78. type IViewListProps = IViewListStateProps &
  79. IViewListDispatchProps &
  80. RouteComponentWithParams
  81. // tslint:disable-next-line:interface-name
  82. interface Catalogue {
  83. description?: string // 资源描述
  84. extConfig?: string // 扩展信息
  85. id?: number
  86. industry?: string // 行业分类
  87. name?: string // 资源名称
  88. originDept?: string // 来源部门
  89. originSystem?: string // 来源系统
  90. parentId?: string
  91. projectId?: number
  92. }
  93. interface IViewListStates {
  94. screenWidth: number
  95. tempFilterViewName: string
  96. filterViewName: string
  97. filterDropdownVisible: boolean
  98. tableSorter: SorterResult<IViewBase>
  99. copyModalVisible: boolean
  100. copyFromView: IViewBase
  101. viewList: IViewBase[]
  102. catalogueModalVisible: boolean
  103. catalogueFromView: ICatalogue
  104. saveCatalogueLoading: boolean
  105. catalogues: ICatalogue[]
  106. selectedCatalogueKeys: string[]
  107. treeLoading: boolean
  108. tableLoading: boolean
  109. }
  110. const { TreeNode, DirectoryTree } = Tree
  111. export class ViewList extends React.PureComponent<IViewListProps,
  112. IViewListStates> {
  113. // @ts-ignore
  114. public state: Readonly<IViewListStates> = {
  115. screenWidth: document.documentElement.clientWidth,
  116. tempFilterViewName: '',
  117. filterViewName: '',
  118. filterDropdownVisible: false,
  119. tableSorter: null,
  120. copyModalVisible: false,
  121. copyFromView: null,
  122. catalogueModalVisible: false,
  123. catalogueFromView: null,
  124. saveCatalogueLoading: false,
  125. catalogues: [],
  126. viewList: [],
  127. treeLoading: false,
  128. tableLoading: false
  129. }
  130. public async componentWillMount() {
  131. const { projectId } = this.props.match.params
  132. await (projectId && this.getCatalogues())
  133. // const parentId = Number(this.state.selectedCatalogueKeys[0]) || null
  134. // tslint:disable-next-line:no-unused-expression
  135. projectId && this.loadViews()
  136. window.addEventListener('resize', this.setScreenWidth, false)
  137. }
  138. private loadViews = async() => {
  139. const { projectId } = this.props.match.params
  140. if (projectId && this.state.selectedCatalogueKeys.length > 0) {
  141. const parentId = Number(this.state.selectedCatalogueKeys[0])
  142. try {
  143. this.setState({ tableLoading: true })
  144. const data = await request(
  145. api.getViewsByParentId +
  146. `?projectId=${projectId}&parentId=${parentId}`,
  147. { method: 'get' }
  148. )
  149. this.setState({
  150. // @ts-ignore
  151. viewList: (data.payload as unknown as IViewBase[]) ?? []
  152. })
  153. } catch (e) {
  154. console.log(e)
  155. } finally {
  156. this.setState({ tableLoading: false })
  157. }
  158. }
  159. }
  160. public componentWillUnmount() {
  161. window.removeEventListener('resize', this.setScreenWidth, false)
  162. }
  163. private setScreenWidth = () => {
  164. this.setState({ screenWidth: document.documentElement.clientWidth })
  165. }
  166. private getFilterViews = memoizeOne(
  167. (viewName: string, views: IViewBase[]) => {
  168. if (!Array.isArray(views) || !views.length) {
  169. return []
  170. }
  171. const regex = new RegExp(viewName, 'gi')
  172. return views.filter(
  173. (v) => v.name.match(regex) || v.description.match(regex)
  174. )
  175. }
  176. )
  177. private static getViewPermission = memoizeOne((project: IProject) => ({
  178. viewPermission: initializePermission(project, 'viewPermission'),
  179. AdminButton: ModulePermission<ButtonProps>(project, 'view', true)(Button),
  180. EditButton: ModulePermission<ButtonProps>(project, 'view', false)(Button)
  181. }))
  182. private getTableColumns = ({
  183. viewPermission,
  184. AdminButton,
  185. EditButton
  186. }: ReturnType<typeof ViewList.getViewPermission>) => {
  187. // const { views } = this.props
  188. const { viewList } = this.state
  189. const {
  190. tempFilterViewName,
  191. filterViewName,
  192. filterDropdownVisible,
  193. tableSorter
  194. } = this.state
  195. const sourceNames = viewList.map(({ sourceName }) => sourceName)
  196. const columns: Array<ColumnProps<IViewBase>> = [
  197. {
  198. title: '名称',
  199. dataIndex: 'name',
  200. filterDropdown: (
  201. <SearchFilterDropdown
  202. placeholder='名称'
  203. value={tempFilterViewName}
  204. onChange={this.filterViewNameChange}
  205. onSearch={this.searchView}
  206. />
  207. ),
  208. filterDropdownVisible,
  209. onFilterDropdownVisibleChange: (visible: boolean) =>
  210. this.setState({ filterDropdownVisible: visible }),
  211. sorter: (a, b) => (a.name > b.name ? 1 : -1),
  212. sortOrder:
  213. tableSorter && tableSorter.columnKey === 'name'
  214. ? tableSorter.order
  215. : void 0
  216. },
  217. {
  218. title: '描述',
  219. dataIndex: 'description'
  220. },
  221. {
  222. title: '数据源',
  223. // title: 'Source',
  224. dataIndex: 'sourceName',
  225. filterMultiple: false,
  226. onFilter: (val, record) => record.sourceName === val,
  227. filters: sourceNames
  228. .filter((name, idx) => sourceNames.indexOf(name) === idx)
  229. .map((name) => ({ text: name, value: name }))
  230. }
  231. ]
  232. if (filterViewName) {
  233. const regex = new RegExp(`(${filterViewName})`, 'gi')
  234. columns[0].render = (text: string) => (
  235. <span
  236. dangerouslySetInnerHTML={{
  237. __html: text.replace(
  238. regex,
  239. `<span class='${utilStyles.highlight}'>$1</span>`
  240. )
  241. }}
  242. />
  243. )
  244. }
  245. if (viewPermission) {
  246. columns.push({
  247. title: '操作',
  248. width: 150,
  249. className: utilStyles.textAlignCenter,
  250. render: (_, record) => (
  251. <span className='ant-table-action-column'>
  252. <Tooltip title='复制'>
  253. <EditButton
  254. icon='copy'
  255. shape='circle'
  256. type='ghost'
  257. onClick={this.copyView(record)}
  258. />
  259. </Tooltip>
  260. <Tooltip title='修改'>
  261. <EditButton
  262. icon='edit'
  263. shape='circle'
  264. type='ghost'
  265. onClick={this.editView(record.id)}
  266. />
  267. </Tooltip>
  268. <Popconfirm
  269. title='确定删除?'
  270. placement='bottom'
  271. onConfirm={this.deleteView(record.id)}
  272. >
  273. <Tooltip title='删除'>
  274. <AdminButton icon='delete' shape='circle' type='ghost' />
  275. </Tooltip>
  276. </Popconfirm>
  277. </span>
  278. )
  279. })
  280. }
  281. return columns
  282. }
  283. private tableChange = (_1, _2, sorter: SorterResult<IViewBase>) => {
  284. this.setState({ tableSorter: sorter })
  285. }
  286. private filterViewNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  287. this.setState({
  288. tempFilterViewName: e.target.value,
  289. filterViewName: ''
  290. })
  291. }
  292. private searchView = (value: string) => {
  293. this.setState({
  294. filterViewName: value,
  295. filterDropdownVisible: false
  296. })
  297. window.event.preventDefault()
  298. }
  299. private basePagination: PaginationConfig = {
  300. defaultPageSize: 20,
  301. showSizeChanger: true
  302. }
  303. private addView = () => {
  304. const { history, match } = this.props
  305. history.push(
  306. `/project/${
  307. match.params.projectId
  308. }/view?parentId=${this.state.selectedCatalogueKeys?.join()}`
  309. )
  310. }
  311. private copyView = (fromView: IViewBase) => () => {
  312. this.setState({
  313. copyModalVisible: true,
  314. copyFromView: fromView
  315. })
  316. }
  317. private copy = (view: IViewBase) => {
  318. const { onCopyView } = this.props
  319. onCopyView(view, () => {
  320. this.setState({
  321. copyModalVisible: false
  322. })
  323. message.info('数据资产 复制成功')
  324. this.loadViews()
  325. })
  326. }
  327. private cancelCopy = () => {
  328. this.setState({ copyModalVisible: false })
  329. }
  330. private editView = (viewId: number) => () => {
  331. const { history, match } = this.props
  332. history.push(`/project/${match.params.projectId}/view/${viewId}`)
  333. }
  334. private deleteView = (viewId: number) => () => {
  335. const { onDeleteView } = this.props
  336. onDeleteView(viewId, () => {
  337. this.loadViews()
  338. })
  339. }
  340. private checkViewUniqueName = (
  341. viewName: string,
  342. resolve: () => void,
  343. reject: (err: string) => void
  344. ) => {
  345. const { currentProject, onCheckName } = this.props
  346. onCheckName(
  347. { name: viewName, projectId: currentProject.id },
  348. resolve,
  349. reject
  350. )
  351. }
  352. private getCatalogues = async() => {
  353. try {
  354. const { projectId } = this.props.match.params
  355. this.setState({ treeLoading: true })
  356. // @ts-ignore
  357. const { payload } = await request(
  358. api.getCatalogues + `?projectId=${projectId}`,
  359. { method: 'GET' }
  360. )
  361. this.setState({
  362. catalogues: payload as unknown as ICatalogue[],
  363. selectedCatalogueKeys: payload?.[0]?.id ? [`${payload?.[0]?.id}`] : []
  364. })
  365. } catch (e) {
  366. console.log()
  367. } finally {
  368. this.setState({ treeLoading: false })
  369. }
  370. }
  371. private handleSaveCatalogue = async(catalogue: ICatalogue) => {
  372. const { projectId } = this.props.match.params
  373. if (!projectId) {
  374. return
  375. }
  376. try {
  377. const parentId = Number(this.state.selectedCatalogueKeys[0]) || null
  378. const catalogueFromView = this.state.catalogueFromView
  379. this.setState({ saveCatalogueLoading: true })
  380. if (catalogueFromView) {
  381. await request(api.updateCatalogue + `/${catalogueFromView.id}`, {
  382. method: 'PUT',
  383. data: {
  384. ...catalogue,
  385. projectId
  386. }
  387. })
  388. } else {
  389. await request(api.createCatalogue, {
  390. method: 'post',
  391. data: {
  392. ...catalogue,
  393. parentId: catalogue.parentId === '-1' ? null : catalogue.parentId,
  394. projectId
  395. }
  396. })
  397. }
  398. this.setState({ catalogueModalVisible: false })
  399. this.getCatalogues()
  400. } finally {
  401. this.setState({
  402. saveCatalogueLoading: false,
  403. catalogueFromView: null
  404. })
  405. }
  406. }
  407. private handleEditCatalogue = (c: ICatalogue) => {
  408. this.setState({
  409. catalogueModalVisible: true,
  410. catalogueFromView: c
  411. })
  412. }
  413. private handleDeleteCatalogue = async(c: ICatalogue) => {
  414. try {
  415. this.setState({ treeLoading: true })
  416. const data = await request(api.deleteCatalogue + `/${c.id}`, {
  417. method: 'DELETE'
  418. })
  419. // @ts-ignore
  420. if (data?.header?.code === 200) {
  421. message.success({ content: '删除成功' })
  422. this.getCatalogues()
  423. } else {
  424. // @ts-ignore
  425. // tslint:disable-next-line:no-unused-expression
  426. data?.header?.msg && message.error({ content: data?.header?.msg })
  427. }
  428. } finally {
  429. this.setState({ treeLoading: false })
  430. }
  431. }
  432. private renderTree = (catalogues: ICatalogue[]) => {
  433. const { selectedCatalogueKeys } = this.state
  434. // tslint:disable-next-line:jsx-wrap-multiline
  435. return (
  436. <>
  437. {catalogues.map((c, idx) => (
  438. <React.Fragment key={c.id ?? idx}>
  439. <div
  440. key={c.id ?? idx}
  441. className={classnames(styles.treeNode, {
  442. [styles.treeNodeSelected]: selectedCatalogueKeys.includes(
  443. `${c.id}`
  444. ),
  445. [styles.treeNodeChild]: !!c.parentId
  446. })}
  447. >
  448. <span
  449. className={styles.treeNodeLeft}
  450. onClick={() => {
  451. this.setState({ selectedCatalogueKeys: [`${c.id}`] }, () => {
  452. this.loadViews()
  453. })
  454. }}
  455. >
  456. <Icon type='folder-open' />
  457. {c.name}
  458. </span>
  459. <Dropdown
  460. overlay={() => (
  461. <Menu>
  462. <Menu.Item
  463. key='0'
  464. onClick={() => this.handleEditCatalogue(c)}
  465. >
  466. 编辑
  467. </Menu.Item>
  468. <Menu.Item key='1'>
  469. <Popconfirm
  470. title='确定删除?'
  471. placement='bottom'
  472. onConfirm={() => this.handleDeleteCatalogue(c)}
  473. >
  474. <a> 删除</a>
  475. </Popconfirm>
  476. </Menu.Item>
  477. </Menu>
  478. )}
  479. trigger={['click']}
  480. >
  481. <Icon type='more' />
  482. </Dropdown>
  483. </div>
  484. <div style={{ marginLeft: 20 }}>
  485. {c.children && this.renderTree(c.children)}
  486. </div>
  487. </React.Fragment>
  488. ))}
  489. </>
  490. )
  491. }
  492. public render() {
  493. const { currentProject, views, loading } = this.props
  494. const { screenWidth, filterViewName, viewList } = this.state
  495. const { viewPermission, AdminButton, EditButton } =
  496. ViewList.getViewPermission(currentProject)
  497. const tableColumns = this.getTableColumns({
  498. viewPermission,
  499. AdminButton,
  500. EditButton
  501. })
  502. const tablePagination: PaginationConfig = {
  503. ...this.basePagination,
  504. simple: screenWidth <= 768
  505. }
  506. const filterViews = this.getFilterViews(filterViewName, viewList)
  507. const {
  508. copyModalVisible,
  509. copyFromView,
  510. catalogueModalVisible,
  511. catalogueFromView
  512. } = this.state
  513. const pathname = this.props.history.location.pathname
  514. return (
  515. <>
  516. <Container>
  517. <Helmet title='数据资产' />
  518. {!pathname.includes('dataManager') && (
  519. <ContainerTitle>
  520. <Row>
  521. <Col span={24} className={utilStyles.shortcut}>
  522. <Breadcrumb className={utilStyles.breadcrumb}>
  523. <Breadcrumb.Item>
  524. <Link to=''>View</Link>
  525. </Breadcrumb.Item>
  526. </Breadcrumb>
  527. <Link to={`/account/organization/${currentProject.orgId}`}>
  528. <i className='iconfont icon-organization' />
  529. </Link>
  530. </Col>
  531. </Row>
  532. </ContainerTitle>
  533. )}
  534. <ContainerBody>
  535. <Box>
  536. <Box.Header>
  537. <Box.Title>
  538. <Icon type='bars' />
  539. 数据资产列表
  540. </Box.Title>
  541. <Box.Tools>
  542. <Tooltip placement='bottom' title='新增'>
  543. <AdminButton
  544. type='primary'
  545. icon='plus'
  546. onClick={this.addView}
  547. />
  548. </Tooltip>
  549. </Box.Tools>
  550. </Box.Header>
  551. <Box.Body>
  552. <div className={styles.treeTableContainer}>
  553. <div className={styles.treeContainer}>
  554. <div className={styles.treeTitle}>
  555. <h6>资源目录列表</h6>
  556. <div
  557. className={styles.treePlusNode}
  558. onClick={() => {
  559. this.setState({ catalogueModalVisible: true })
  560. }}
  561. >
  562. <Icon type='plus' />
  563. </div>
  564. </div>
  565. <div className={styles.treeContent}>
  566. <Spin spinning={this.state.treeLoading}>
  567. {this.renderTree(this.state.catalogues)}
  568. </Spin>
  569. </div>
  570. </div>
  571. <Table
  572. style={{ flex: 1 }}
  573. bordered
  574. rowKey='id'
  575. loading={this.state.tableLoading}
  576. dataSource={filterViews}
  577. columns={tableColumns}
  578. pagination={tablePagination}
  579. onChange={this.tableChange}
  580. />
  581. </div>
  582. <div style={{ padding: 20 }} />
  583. </Box.Body>
  584. </Box>
  585. </ContainerBody>
  586. </Container>
  587. <CopyModal
  588. visible={copyModalVisible}
  589. loading={loading.copy}
  590. fromView={copyFromView}
  591. onCheckUniqueName={this.checkViewUniqueName}
  592. onCopy={this.copy}
  593. onCancel={this.cancelCopy}
  594. />
  595. <CatalogueModal
  596. catalogues={this.state.catalogues}
  597. selectedCatalogueKeys={this.state.selectedCatalogueKeys}
  598. visible={catalogueModalVisible}
  599. loading={this.state.saveCatalogueLoading}
  600. fromView={catalogueFromView}
  601. onCheckUniqueName={this.checkViewUniqueName}
  602. onSave={this.handleSaveCatalogue}
  603. onCancel={() => this.setState({ catalogueModalVisible: false })}
  604. />
  605. </>
  606. )
  607. }
  608. }
  609. const mapDispatchToProps = (dispatch: Dispatch<ViewActionType>) => ({
  610. onLoadViews: (projectId, parentId) =>
  611. dispatch(ViewActions.loadViews(projectId, parentId)),
  612. onDeleteView: (viewId, resolve) =>
  613. dispatch(ViewActions.deleteView(viewId, resolve)),
  614. onCopyView: (view, resolve) => dispatch(ViewActions.copyView(view, resolve)),
  615. // @ts-ignore
  616. onCheckName: (data, resolve, reject) =>
  617. // @ts-ignore
  618. dispatch(checkNameUniqueAction('view', data, resolve, reject))
  619. })
  620. const mapStateToProps = createStructuredSelector({
  621. views: makeSelectViews(),
  622. currentProject: makeSelectCurrentProject(),
  623. loading: makeSelectLoading()
  624. })
  625. const withConnect = connect<IViewListStateProps,
  626. IViewListDispatchProps,
  627. RouteComponentWithParams>(mapStateToProps, mapDispatchToProps)
  628. const withReducer = injectReducer({ key: 'view', reducer })
  629. const withSaga = injectSaga({ key: 'view', saga: sagas })
  630. export default compose(withReducer, withSaga, withConnect)(ViewList)