index.tsx 19 KB

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