index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  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 html2canvas from 'html2canvas'
  25. import { compose } from 'redux'
  26. import injectReducer from 'utils/injectReducer'
  27. import injectSaga from 'utils/injectSaga'
  28. import reducer from './reducer'
  29. import controlReducer from 'app/containers/ControlPanel/reducer'
  30. import saga from './sagas'
  31. import Container, { ContainerTitle } from 'components/Container'
  32. import {
  33. getMappingLinkage,
  34. processLinkage,
  35. removeLinkage
  36. } from 'components/Linkages'
  37. import DashboardItem from 'containers/Dashboard/components/DashboardItem'
  38. import FullScreenPanel from './FullScreenPanel'
  39. import { Responsive, WidthProvider } from 'react-grid-layout'
  40. import { ChartTypes } from 'containers/Widget/config/chart/ChartTypes'
  41. import {
  42. IFilter,
  43. IDistinctValueReqeustParams
  44. } from 'app/components/Control/types'
  45. import GlobalControlPanel from 'app/containers/ControlPanel/Global'
  46. import DownloadList from 'components/DownloadList'
  47. import { getValidColumnValue } from 'app/components/Control/util'
  48. import HeadlessBrowserIdentifier from 'share/components/HeadlessBrowserIdentifier'
  49. import { Row, Col } from 'antd'
  50. import { querystring } from '../../util'
  51. import DashboardActions from './actions'
  52. import ControlActions from 'app/containers/ControlPanel/actions'
  53. const {
  54. getDashboard,
  55. getWidget,
  56. getResultset,
  57. getBatchDataWithControlValues,
  58. setIndividualDashboard,
  59. loadWidgetCsv,
  60. loadSelectOptions,
  61. resizeDashboardItem,
  62. resizeAllDashboardItem,
  63. renderChartError,
  64. drillDashboardItem,
  65. deleteDrillHistory,
  66. selectDashboardItemChart,
  67. loadDownloadList,
  68. downloadFile,
  69. initiateDownloadTask,
  70. sendShareParams,
  71. setFullScreenPanelItemId
  72. } = DashboardActions
  73. const { setSelectOptions } = ControlActions
  74. import {
  75. makeSelectDashboard,
  76. makeSelectTitle,
  77. makeSelectWidgets,
  78. makeSelectFormedViews,
  79. makeSelectItems,
  80. makeSelectItemsInfo,
  81. makeSelectLinkages,
  82. makeSelectDownloadList,
  83. makeSelectShareParams
  84. } from './selectors'
  85. import { makeSelectLoginLoading, makeSelectVizType } from '../App/selectors'
  86. import {
  87. GRID_COLS,
  88. GRID_ROW_HEIGHT,
  89. GRID_ITEM_MARGIN,
  90. GRID_BREAKPOINTS,
  91. DOWNLOAD_LIST_POLLING_FREQUENCY
  92. } from 'app/globalConstants'
  93. import styles from 'app/containers/Dashboard/Dashboard.less'
  94. import {
  95. IQueryConditions,
  96. IDataRequestParams,
  97. QueryVariable
  98. } from 'app/containers/Dashboard/types'
  99. import { RenderType } from 'containers/Widget/components/Widget'
  100. import { getShareClientId } from 'share/util'
  101. import { DashboardItemStatus } from './constants'
  102. import { IWidgetFormed } from 'app/containers/Widget/types'
  103. import {
  104. ControlPanelLayoutTypes,
  105. ControlPanelTypes
  106. } from 'app/components/Control/constants'
  107. import { IDrillDetail } from 'components/DataDrill/types'
  108. import { IShareFormedViews } from 'app/containers/View/types'
  109. const ResponsiveReactGridLayout = WidthProvider(Responsive)
  110. type MappedStates = ReturnType<typeof mapStateToProps>
  111. type MappedDispatches = ReturnType<typeof mapDispatchToProps>
  112. type IDashboardProps = MappedStates & MappedDispatches
  113. interface IDashboardStates {
  114. shareToken: string
  115. modalLoading: boolean
  116. interactingStatus: { [itemId: number]: boolean }
  117. headlessBrowserRenderSign: boolean
  118. }
  119. export class Share extends React.Component<IDashboardProps, IDashboardStates> {
  120. constructor(props) {
  121. super(props)
  122. this.state = {
  123. shareToken: '',
  124. modalLoading: false,
  125. interactingStatus: {},
  126. headlessBrowserRenderSign: false
  127. }
  128. }
  129. private interactCallbacks: object = {}
  130. private interactingLinkagers: object = {}
  131. private interactGlobalFilters: object = {}
  132. private resizeSign: number = 0
  133. private shareClientId: string = getShareClientId()
  134. private downloadListPollingTimer: number
  135. private loadShareContent = (shareToken: string) => {
  136. const {
  137. vizType,
  138. onLoadDashboard,
  139. onLoadWidget,
  140. onSetIndividualDashboard
  141. } = this.props
  142. switch (vizType) {
  143. case 'dashboard':
  144. onLoadDashboard(shareToken, () => null)
  145. break
  146. case 'widget':
  147. onLoadWidget(
  148. shareToken,
  149. (widget, formedViews) => {
  150. onSetIndividualDashboard(widget, formedViews, shareToken)
  151. },
  152. () => null
  153. )
  154. break
  155. }
  156. }
  157. public componentDidMount() {
  158. // urlparse
  159. const { shareToken, ...rest } = querystring(
  160. window.location.search.substr(1)
  161. )
  162. this.setState({ shareToken })
  163. this.loadShareContent(shareToken)
  164. this.initPolling(shareToken)
  165. this.props.onSendShareParams(rest)
  166. window.addEventListener('resize', this.onWindowResize, false)
  167. }
  168. public componentWillReceiveProps(nextProps: IDashboardProps) {
  169. const { currentItems, currentItemsInfo } = nextProps
  170. if (currentItemsInfo) {
  171. const initialedItems = Object.values(currentItemsInfo).filter((info) =>
  172. [DashboardItemStatus.Fulfilled, DashboardItemStatus.Error].includes(
  173. info.status
  174. )
  175. )
  176. if (initialedItems.length === currentItems.length) {
  177. // FIXME
  178. setTimeout(() => {
  179. this.setState({
  180. headlessBrowserRenderSign: true
  181. })
  182. }, 5000)
  183. }
  184. }
  185. }
  186. public componentWillUnmount() {
  187. window.removeEventListener('resize', this.onWindowResize, false)
  188. if (this.downloadListPollingTimer) {
  189. clearInterval(this.downloadListPollingTimer)
  190. }
  191. }
  192. private initPolling = (token) => {
  193. this.props.onLoadDownloadList(this.shareClientId, token)
  194. this.downloadListPollingTimer = window.setInterval(() => {
  195. this.props.onLoadDownloadList(this.shareClientId, token)
  196. }, DOWNLOAD_LIST_POLLING_FREQUENCY)
  197. }
  198. private initiateWidgetDownloadTask = (itemId: number) => {
  199. this.props.onInitiateDownloadTask(this.shareClientId, itemId)
  200. }
  201. private onBreakpointChange = () => {
  202. this.onWindowResize()
  203. }
  204. private onWindowResize = () => {
  205. if (this.resizeSign) {
  206. clearTimeout(this.resizeSign)
  207. }
  208. this.resizeSign = window.setTimeout(() => {
  209. this.props.onResizeAllDashboardItem()
  210. clearTimeout(this.resizeSign)
  211. this.resizeSign = 0
  212. }, 500)
  213. }
  214. private checkInteract = (itemId: number) => {
  215. const { linkages } = this.props
  216. const isInteractiveItem = linkages.some((lts) => {
  217. const { trigger } = lts
  218. const triggerId = +trigger[0]
  219. return triggerId === itemId
  220. })
  221. return isInteractiveItem
  222. }
  223. private doInteract = (itemId: number, triggerData) => {
  224. const { currentItems, onLoadResultset, linkages } = this.props
  225. const mappingLinkage = getMappingLinkage(itemId, linkages)
  226. this.interactingLinkagers = processLinkage(
  227. itemId,
  228. triggerData,
  229. mappingLinkage,
  230. this.interactingLinkagers
  231. )
  232. Object.keys(mappingLinkage).forEach((linkagerItemId) => {
  233. const item = currentItems.find((ci) => ci.id === +linkagerItemId)
  234. const { filters, variables } = this.interactingLinkagers[linkagerItemId]
  235. onLoadResultset('rerender', +linkagerItemId, {
  236. linkageFilters: Object.values(filters).reduce<string[]>(
  237. (arr, f: string[]) => arr.concat(...f),
  238. []
  239. ),
  240. linkageVariables: Object.values(variables).reduce<QueryVariable>(
  241. (arr, p: QueryVariable) => arr.concat(...p),
  242. []
  243. )
  244. })
  245. })
  246. this.setState({
  247. interactingStatus: {
  248. ...this.state.interactingStatus,
  249. [itemId]: true
  250. }
  251. })
  252. }
  253. private turnOffInteract = (itemId) => {
  254. const { linkages, currentItems, onLoadResultset } = this.props
  255. const refreshItemIds = removeLinkage(
  256. itemId,
  257. linkages,
  258. this.interactingLinkagers
  259. )
  260. refreshItemIds.forEach((linkagerItemId) => {
  261. const item = currentItems.find((ci) => ci.id === linkagerItemId)
  262. const { filters, variables } = this.interactingLinkagers[linkagerItemId]
  263. onLoadResultset('rerender', linkagerItemId, {
  264. linkageFilters: Object.values(filters).reduce<string[]>(
  265. (arr, f: string[]) => arr.concat(...f),
  266. []
  267. ),
  268. linkageVariables: Object.values(variables).reduce<QueryVariable>(
  269. (arr, p: QueryVariable) => arr.concat(...p),
  270. []
  271. )
  272. })
  273. })
  274. this.setState(
  275. {
  276. interactingStatus: {
  277. ...this.state.interactingStatus,
  278. [itemId]: false
  279. }
  280. },
  281. () => {
  282. const item = currentItems.find((ci) => ci.id === itemId)
  283. onLoadResultset('clear', itemId)
  284. }
  285. )
  286. }
  287. private getControlSelectOptions = (
  288. controlKey: string,
  289. useOptions: boolean,
  290. paramsOrOptions,
  291. itemId?: number
  292. ) => {
  293. if (useOptions) {
  294. this.props.onSetSelectOptions(controlKey, paramsOrOptions, itemId)
  295. } else {
  296. this.props.onLoadSelectOptions(controlKey, paramsOrOptions, itemId)
  297. }
  298. }
  299. private dataDrill = (drillDetail) => {
  300. const { onDrillDashboardItem, onLoadResultset } = this.props
  301. const {
  302. itemId,
  303. cols,
  304. rows,
  305. type,
  306. groups,
  307. filters,
  308. currentGroup
  309. } = drillDetail
  310. const currentDrillStatus: IDrillDetail = {
  311. cols,
  312. rows,
  313. type,
  314. groups,
  315. filters,
  316. currentGroup
  317. }
  318. onDrillDashboardItem(itemId, currentDrillStatus)
  319. onLoadResultset('rerender', itemId, {
  320. drillStatus: currentDrillStatus
  321. })
  322. }
  323. private selectDrillHistory = (history, item, itemId) => {
  324. const { onLoadResultset, onDeleteDrillHistory } = this.props
  325. setTimeout(() => {
  326. if (history) {
  327. onLoadResultset('rerender', itemId, {
  328. drillStatus: history
  329. })
  330. } else {
  331. onLoadResultset('rerender', itemId)
  332. }
  333. }, 50)
  334. onDeleteDrillHistory(itemId, item)
  335. }
  336. private selectChartsItems = (itemId, renderType, selectedItems) => {
  337. const { onSelectDashboardItemChart } = this.props
  338. onSelectDashboardItemChart(itemId, renderType, selectedItems)
  339. }
  340. private loadDownloadList = () => {
  341. this.props.onLoadDownloadList(this.shareClientId, this.state.shareToken)
  342. }
  343. private downloadFile = (id) => {
  344. this.props.onDownloadFile(id, this.shareClientId, this.state.shareToken)
  345. }
  346. public render() {
  347. const {
  348. dashboard,
  349. title,
  350. currentItems,
  351. currentItemsInfo,
  352. widgets,
  353. formedViews,
  354. linkages,
  355. downloadList,
  356. loginLoading,
  357. onLoadResultset,
  358. onLoadBatchDataWithControlValues,
  359. onResizeDashboardItem,
  360. onRenderChartError,
  361. onSetFullScreenPanelItemId
  362. } = this.props
  363. const {
  364. shareToken,
  365. interactingStatus,
  366. headlessBrowserRenderSign
  367. } = this.state
  368. let grids = null
  369. if (currentItems) {
  370. const itemblocks: React.ReactNode[] = []
  371. const layouts = { lg: [] }
  372. currentItems.forEach((dashboardItem) => {
  373. const {
  374. id,
  375. x,
  376. y,
  377. width,
  378. height,
  379. widgetId,
  380. polling,
  381. frequency
  382. } = dashboardItem
  383. const {
  384. datasource,
  385. loading,
  386. downloadCsvLoading,
  387. renderType,
  388. queryConditions,
  389. selectedItems,
  390. errorMessage
  391. } = currentItemsInfo[id]
  392. const widget = widgets.find((w) => w.id === widgetId)
  393. const view = formedViews[widget.viewId]
  394. const interacting = interactingStatus[id] || false
  395. const drillHistory = queryConditions.drillHistory
  396. const isTrigger =
  397. linkages && linkages.length
  398. ? linkages
  399. .map((linkage) => linkage.trigger[0])
  400. .some((tr) => tr === String(id))
  401. : false
  402. itemblocks.push(
  403. <div key={id}>
  404. <DashboardItem
  405. itemId={id}
  406. widget={widget}
  407. widgets={widgets}
  408. formedViews={formedViews}
  409. view={view}
  410. isTrigger={isTrigger}
  411. datasource={datasource}
  412. loading={loading}
  413. polling={polling}
  414. onDrillData={this.dataDrill}
  415. onSelectDrillHistory={this.selectDrillHistory}
  416. interacting={interacting}
  417. frequency={frequency}
  418. shareToken={widget.dataToken}
  419. downloadCsvLoading={downloadCsvLoading}
  420. renderType={renderType}
  421. queryConditions={queryConditions}
  422. errorMessage={errorMessage}
  423. selectedItems={selectedItems || []}
  424. container="share"
  425. onLoadData={onLoadResultset}
  426. onResizeDashboardItem={onResizeDashboardItem}
  427. onRenderChartError={onRenderChartError}
  428. onDownloadCsv={this.initiateWidgetDownloadTask}
  429. onTurnOffInteract={this.turnOffInteract}
  430. onCheckTableInteract={this.checkInteract}
  431. onDoTableInteract={this.doInteract}
  432. onShowFullScreen={onSetFullScreenPanelItemId}
  433. onSelectChartsItems={this.selectChartsItems}
  434. onGetControlOptions={this.getControlSelectOptions}
  435. onControlSearch={onLoadBatchDataWithControlValues}
  436. />
  437. </div>
  438. )
  439. layouts.lg.push({
  440. x,
  441. y,
  442. w: width,
  443. h: height,
  444. i: `${id}`
  445. })
  446. })
  447. grids = (
  448. <ResponsiveReactGridLayout
  449. className="layout"
  450. style={{ marginTop: '-16px' }}
  451. rowHeight={GRID_ROW_HEIGHT}
  452. margin={[GRID_ITEM_MARGIN, GRID_ITEM_MARGIN]}
  453. breakpoints={GRID_BREAKPOINTS}
  454. cols={GRID_COLS}
  455. layouts={layouts}
  456. onBreakpointChange={this.onBreakpointChange}
  457. measureBeforeMount={false}
  458. useCSSTransforms={false}
  459. isDraggable={false}
  460. isResizable={false}
  461. >
  462. {itemblocks}
  463. </ResponsiveReactGridLayout>
  464. )
  465. } else {
  466. grids = (
  467. <div className={styles.shareContentEmpty}>
  468. <h3>数据加载中……</h3>
  469. </div>
  470. )
  471. }
  472. const headlessBrowserRenderParentNode = document.getElementById('app')
  473. return (
  474. <Container>
  475. <Helmet title={title} />
  476. <ContainerTitle>
  477. <Row>
  478. <Col span={24}>
  479. <h2 className={styles.shareTitle}>{title}</h2>
  480. <div className={styles.shareDownloadListToggle}>
  481. <DownloadList
  482. downloadList={downloadList}
  483. onLoadDownloadList={this.loadDownloadList}
  484. onDownloadFile={this.downloadFile}
  485. />
  486. </div>
  487. </Col>
  488. </Row>
  489. <GlobalControlPanel
  490. currentDashboard={dashboard}
  491. currentItems={currentItems}
  492. formedViews={formedViews}
  493. layoutType={ControlPanelLayoutTypes.Dashboard}
  494. onGetOptions={this.getControlSelectOptions}
  495. onSearch={onLoadBatchDataWithControlValues}
  496. />
  497. </ContainerTitle>
  498. {grids}
  499. <div className={styles.gridBottom} />
  500. <FullScreenPanel
  501. currentDashboard={dashboard}
  502. widgets={widgets}
  503. formedViews={formedViews}
  504. currentItems={currentItems}
  505. currentItemsInfo={currentItemsInfo}
  506. onLoadData={onLoadResultset}
  507. onGetOptions={this.getControlSelectOptions}
  508. onSearch={onLoadBatchDataWithControlValues}
  509. />
  510. <HeadlessBrowserIdentifier
  511. renderSign={headlessBrowserRenderSign}
  512. parentNode={headlessBrowserRenderParentNode}
  513. />
  514. </Container>
  515. )
  516. }
  517. }
  518. const mapStateToProps = createStructuredSelector({
  519. vizType: makeSelectVizType(),
  520. dashboard: makeSelectDashboard(),
  521. title: makeSelectTitle(),
  522. widgets: makeSelectWidgets(),
  523. formedViews: makeSelectFormedViews(),
  524. currentItems: makeSelectItems(),
  525. currentItemsInfo: makeSelectItemsInfo(),
  526. linkages: makeSelectLinkages(),
  527. downloadList: makeSelectDownloadList(),
  528. shareParams: makeSelectShareParams(),
  529. loginLoading: makeSelectLoginLoading()
  530. })
  531. export function mapDispatchToProps(dispatch) {
  532. return {
  533. onLoadDashboard: (token: string, reject: (err) => void) =>
  534. dispatch(getDashboard(token, reject)),
  535. onLoadWidget: (
  536. token: string,
  537. resolve: (widget: IWidgetFormed, formedViews: IShareFormedViews) => void,
  538. reject: (err) => void
  539. ) => dispatch(getWidget(token, resolve, reject)),
  540. onLoadResultset: (
  541. renderType: RenderType,
  542. itemId: number,
  543. queryConditions?: Partial<IQueryConditions>
  544. ) => dispatch(getResultset(renderType, itemId, queryConditions)),
  545. onLoadBatchDataWithControlValues: (
  546. type: ControlPanelTypes,
  547. relatedItems: number[],
  548. formValues?: object,
  549. itemId?: number
  550. ) =>
  551. dispatch(
  552. getBatchDataWithControlValues(type, relatedItems, formValues, itemId)
  553. ),
  554. onSetIndividualDashboard: (
  555. widget: IWidgetFormed,
  556. formedViews: IShareFormedViews,
  557. token: string
  558. ) => dispatch(setIndividualDashboard(widget, formedViews, token)),
  559. onLoadWidgetCsv: (
  560. itemId: number,
  561. requestParams: IDataRequestParams,
  562. dataToken: string
  563. ) => dispatch(loadWidgetCsv(itemId, requestParams, dataToken)),
  564. onLoadSelectOptions: (
  565. controlKey: string,
  566. reqeustParams: { [viewId: string]: IDistinctValueReqeustParams },
  567. itemId: number
  568. ) => dispatch(loadSelectOptions(controlKey, reqeustParams, itemId)),
  569. onSetSelectOptions: (controlKey: string, options: any[], itemId?: number) =>
  570. dispatch(setSelectOptions(controlKey, options, itemId)),
  571. onResizeDashboardItem: (itemId: number) =>
  572. dispatch(resizeDashboardItem(itemId)),
  573. onResizeAllDashboardItem: () => dispatch(resizeAllDashboardItem()),
  574. onRenderChartError: (itemId: number, error: Error) =>
  575. dispatch(renderChartError(itemId, error)),
  576. onDrillDashboardItem: (itemId: number, drillHistory) =>
  577. dispatch(drillDashboardItem(itemId, drillHistory)),
  578. onDeleteDrillHistory: (itemId: number, index: number) =>
  579. dispatch(deleteDrillHistory(itemId, index)),
  580. onSelectDashboardItemChart: (
  581. itemId: number,
  582. renderType: RenderType,
  583. selectedItems: number[]
  584. ) => dispatch(selectDashboardItemChart(itemId, renderType, selectedItems)),
  585. onInitiateDownloadTask: (shareClientId: string, itemId?: number) =>
  586. dispatch(initiateDownloadTask(shareClientId, itemId)),
  587. onLoadDownloadList: (shareClinetId: string, token: string) =>
  588. dispatch(loadDownloadList(shareClinetId, token)),
  589. onDownloadFile: (id: number, shareClientId: string, token: string) =>
  590. dispatch(downloadFile(id, shareClientId, token)),
  591. onSendShareParams: (params: object) => dispatch(sendShareParams(params)),
  592. onSetFullScreenPanelItemId: (itemId: number) =>
  593. dispatch(setFullScreenPanelItemId(itemId))
  594. }
  595. }
  596. const withConnect = connect(mapStateToProps, mapDispatchToProps)
  597. const withReducer = injectReducer({ key: 'shareDashboard', reducer })
  598. const withSaga = injectSaga({ key: 'shareDashboard', saga })
  599. const withControlReducer = injectReducer({
  600. key: 'control',
  601. reducer: controlReducer
  602. })
  603. export default compose(
  604. withReducer,
  605. withControlReducer,
  606. withSaga,
  607. withConnect
  608. )(Share)