index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  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. })
  325. }
  326. private cancelCopy = () => {
  327. this.setState({ copyModalVisible: false })
  328. }
  329. private editView = (viewId: number) => () => {
  330. const { history, match } = this.props
  331. history.push(`/project/${match.params.projectId}/view/${viewId}`)
  332. }
  333. private deleteView = (viewId: number) => () => {
  334. const { onDeleteView } = this.props
  335. onDeleteView(viewId, () => {
  336. this.loadViews()
  337. })
  338. }
  339. private checkViewUniqueName = (
  340. viewName: string,
  341. resolve: () => void,
  342. reject: (err: string) => void
  343. ) => {
  344. const { currentProject, onCheckName } = this.props
  345. onCheckName(
  346. { name: viewName, projectId: currentProject.id },
  347. resolve,
  348. reject
  349. )
  350. }
  351. private getCatalogues = async() => {
  352. try {
  353. const { projectId } = this.props.match.params
  354. this.setState({ treeLoading: true })
  355. // @ts-ignore
  356. const { payload } = await request(
  357. api.getCatalogues + `?projectId=${projectId}`,
  358. { method: 'GET' }
  359. )
  360. this.setState({
  361. catalogues: payload as unknown as ICatalogue[],
  362. selectedCatalogueKeys: payload?.[0]?.id ? [`${payload?.[0]?.id}`] : []
  363. })
  364. } catch (e) {
  365. console.log()
  366. } finally {
  367. this.setState({ treeLoading: false })
  368. }
  369. }
  370. private handleSaveCatalogue = async(catalogue: ICatalogue) => {
  371. const { projectId } = this.props.match.params
  372. if (!projectId) {
  373. return
  374. }
  375. try {
  376. const parentId = Number(this.state.selectedCatalogueKeys[0]) || null
  377. const catalogueFromView = this.state.catalogueFromView
  378. this.setState({ saveCatalogueLoading: true })
  379. if (catalogueFromView) {
  380. await request(api.updateCatalogue + `/${catalogueFromView.id}`, {
  381. method: 'PUT',
  382. data: {
  383. ...catalogue,
  384. projectId
  385. }
  386. })
  387. } else {
  388. await request(api.createCatalogue, {
  389. method: 'post',
  390. data: {
  391. ...catalogue,
  392. parentId: catalogue.parentId === '-1' ? null : catalogue.parentId,
  393. projectId
  394. }
  395. })
  396. }
  397. this.setState({ catalogueModalVisible: false })
  398. this.getCatalogues()
  399. } finally {
  400. this.setState({
  401. saveCatalogueLoading: false,
  402. catalogueFromView: null
  403. })
  404. }
  405. }
  406. private handleEditCatalogue = (c: ICatalogue) => {
  407. this.setState({
  408. catalogueModalVisible: true,
  409. catalogueFromView: c
  410. })
  411. }
  412. private handleDeleteCatalogue = async(c: ICatalogue) => {
  413. try {
  414. this.setState({ treeLoading: true })
  415. const data = await request(api.deleteCatalogue + `/${c.id}`, {
  416. method: 'DELETE'
  417. })
  418. // @ts-ignore
  419. if (data?.header?.code === 200) {
  420. message.success({ content: '删除成功' })
  421. this.getCatalogues()
  422. } else {
  423. // @ts-ignore
  424. // tslint:disable-next-line:no-unused-expression
  425. data?.header?.msg && message.error({ content: data?.header?.msg })
  426. }
  427. } finally {
  428. this.setState({ treeLoading: false })
  429. }
  430. }
  431. private renderTree = (catalogues: ICatalogue[]) => {
  432. const { selectedCatalogueKeys } = this.state
  433. // tslint:disable-next-line:jsx-wrap-multiline
  434. return (
  435. <>
  436. {catalogues.map((c, idx) => (
  437. <React.Fragment key={c.id ?? idx}>
  438. <div
  439. key={c.id ?? idx}
  440. className={classnames(styles.treeNode, {
  441. [styles.treeNodeSelected]: selectedCatalogueKeys.includes(
  442. `${c.id}`
  443. ),
  444. [styles.treeNodeChild]: !!c.parentId
  445. })}
  446. >
  447. <span
  448. className={styles.treeNodeLeft}
  449. onClick={() => {
  450. this.setState({ selectedCatalogueKeys: [`${c.id}`] }, () => {
  451. this.loadViews()
  452. })
  453. }}
  454. >
  455. <Icon type='folder-open' />
  456. {c.name}
  457. </span>
  458. <Dropdown
  459. overlay={() => (
  460. <Menu>
  461. <Menu.Item
  462. key='0'
  463. onClick={() => this.handleEditCatalogue(c)}
  464. >
  465. 编辑
  466. </Menu.Item>
  467. <Menu.Item key='1'>
  468. <Popconfirm
  469. title='确定删除?'
  470. placement='bottom'
  471. onConfirm={() => this.handleDeleteCatalogue(c)}
  472. >
  473. <a> 删除</a>
  474. </Popconfirm>
  475. </Menu.Item>
  476. </Menu>
  477. )}
  478. trigger={['click']}
  479. >
  480. <Icon type='more' />
  481. </Dropdown>
  482. </div>
  483. <div style={{ marginLeft: 20 }}>
  484. {c.children && this.renderTree(c.children)}
  485. </div>
  486. </React.Fragment>
  487. ))}
  488. </>
  489. )
  490. }
  491. public render() {
  492. const { currentProject, views, loading } = this.props
  493. const { screenWidth, filterViewName, viewList } = this.state
  494. const { viewPermission, AdminButton, EditButton } =
  495. ViewList.getViewPermission(currentProject)
  496. const tableColumns = this.getTableColumns({
  497. viewPermission,
  498. AdminButton,
  499. EditButton
  500. })
  501. const tablePagination: PaginationConfig = {
  502. ...this.basePagination,
  503. simple: screenWidth <= 768
  504. }
  505. const filterViews = this.getFilterViews(filterViewName, viewList)
  506. const {
  507. copyModalVisible,
  508. copyFromView,
  509. catalogueModalVisible,
  510. catalogueFromView
  511. } = this.state
  512. const pathname = this.props.history.location.pathname
  513. return (
  514. <>
  515. <Container>
  516. <Helmet title='数据资产' />
  517. {!pathname.includes('dataManager') && (
  518. <ContainerTitle>
  519. <Row>
  520. <Col span={24} className={utilStyles.shortcut}>
  521. <Breadcrumb className={utilStyles.breadcrumb}>
  522. <Breadcrumb.Item>
  523. <Link to=''>View</Link>
  524. </Breadcrumb.Item>
  525. </Breadcrumb>
  526. <Link to={`/account/organization/${currentProject.orgId}`}>
  527. <i className='iconfont icon-organization' />
  528. </Link>
  529. </Col>
  530. </Row>
  531. </ContainerTitle>
  532. )}
  533. <ContainerBody>
  534. <Box>
  535. <Box.Header>
  536. <Box.Title>
  537. <Icon type='bars' />
  538. 数据资产列表
  539. </Box.Title>
  540. <Box.Tools>
  541. <Tooltip placement='bottom' title='新增'>
  542. <AdminButton
  543. type='primary'
  544. icon='plus'
  545. onClick={this.addView}
  546. />
  547. </Tooltip>
  548. </Box.Tools>
  549. </Box.Header>
  550. <Box.Body>
  551. <div className={styles.treeTableContainer}>
  552. <div className={styles.treeContainer}>
  553. <div className={styles.treeTitle}>
  554. <h6>资源目录列表</h6>
  555. <div
  556. className={styles.treePlusNode}
  557. onClick={() => {
  558. this.setState({ catalogueModalVisible: true })
  559. }}
  560. >
  561. <Icon type='plus' />
  562. </div>
  563. </div>
  564. <div className={styles.treeContent}>
  565. <Spin spinning={this.state.treeLoading}>
  566. {this.renderTree(this.state.catalogues)}
  567. </Spin>
  568. </div>
  569. </div>
  570. <Table
  571. style={{ flex: 1 }}
  572. bordered
  573. rowKey='id'
  574. loading={this.state.tableLoading}
  575. dataSource={filterViews}
  576. columns={tableColumns}
  577. pagination={tablePagination}
  578. onChange={this.tableChange}
  579. />
  580. </div>
  581. <div style={{ padding: 20 }} />
  582. </Box.Body>
  583. </Box>
  584. </ContainerBody>
  585. </Container>
  586. <CopyModal
  587. visible={copyModalVisible}
  588. loading={loading.copy}
  589. fromView={copyFromView}
  590. onCheckUniqueName={this.checkViewUniqueName}
  591. onCopy={this.copy}
  592. onCancel={this.cancelCopy}
  593. />
  594. <CatalogueModal
  595. catalogues={this.state.catalogues}
  596. selectedCatalogueKeys={this.state.selectedCatalogueKeys}
  597. visible={catalogueModalVisible}
  598. loading={this.state.saveCatalogueLoading}
  599. fromView={catalogueFromView}
  600. onCheckUniqueName={this.checkViewUniqueName}
  601. onSave={this.handleSaveCatalogue}
  602. onCancel={() => this.setState({ catalogueModalVisible: false })}
  603. />
  604. </>
  605. )
  606. }
  607. }
  608. const mapDispatchToProps = (dispatch: Dispatch<ViewActionType>) => ({
  609. onLoadViews: (projectId, parentId) =>
  610. dispatch(ViewActions.loadViews(projectId, parentId)),
  611. onDeleteView: (viewId, resolve) =>
  612. dispatch(ViewActions.deleteView(viewId, resolve)),
  613. onCopyView: (view, resolve) => dispatch(ViewActions.copyView(view, resolve)),
  614. // @ts-ignore
  615. onCheckName: (data, resolve, reject) =>
  616. // @ts-ignore
  617. dispatch(checkNameUniqueAction('view', data, resolve, reject))
  618. })
  619. const mapStateToProps = createStructuredSelector({
  620. views: makeSelectViews(),
  621. currentProject: makeSelectCurrentProject(),
  622. loading: makeSelectLoading()
  623. })
  624. const withConnect = connect<IViewListStateProps,
  625. IViewListDispatchProps,
  626. RouteComponentWithParams>(mapStateToProps, mapDispatchToProps)
  627. const withReducer = injectReducer({ key: 'view', reducer })
  628. const withSaga = injectSaga({ key: 'view', saga: sagas })
  629. export default compose(withReducer, withSaga, withConnect)(ViewList)