index.tsx 20 KB

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