index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import React from 'react'
  2. import classnames from 'classnames'
  3. import { ViewModelVisualTypes } from 'containers/View/constants'
  4. import DropboxItem from './DropboxItem'
  5. import DropboxContent from './DropboxContent'
  6. import ColorPanel from '../ColorPanel'
  7. import SizePanel from '../SizePanel'
  8. import { IChartInfo, WidgetMode } from '../../Widget'
  9. import { IFieldConfig } from '../../Config/Field'
  10. import { IFieldFormatConfig } from '../../Config/Format'
  11. import { IFieldSortConfig, FieldSortTypes } from '../../Config/Sort'
  12. import { decodeMetricName } from '../../util'
  13. import { Popover, Icon } from 'antd'
  14. import { IFilter } from 'app/components/Control/types'
  15. const styles = require('../Workbench.less')
  16. export type DragType = 'category' | 'value'
  17. export type DropboxType = DragType | 'all'
  18. export type DropboxItemType = DragType | 'add'
  19. export type DropType = 'outside' | 'inside' | 'unmoved'
  20. export type AggregatorType = 'sum' | 'avg' | 'count' | 'COUNTDISTINCT' | 'max' | 'min' | 'median' | 'var' | 'dev'
  21. interface IDataColumn {
  22. name: string
  23. from?: string
  24. sort?: IFieldSortConfig
  25. agg?: AggregatorType
  26. field?: IFieldConfig
  27. format?: IFieldFormatConfig
  28. }
  29. export interface IDataParamSource extends IDataColumn {
  30. type: DragType
  31. visualType: ViewModelVisualTypes
  32. title?: string
  33. chart?: IChartInfo
  34. config?: IDataParamConfig
  35. }
  36. export interface IDragItem extends IDataParamSource {
  37. checked?: boolean
  38. }
  39. export interface IDataParamConfig {
  40. actOn?: string
  41. values?: {
  42. [key: string]: string
  43. },
  44. sql?: string
  45. sqlModel?: IFilter[]
  46. filterSource?: any
  47. field?: {
  48. alias: string,
  49. desc: string
  50. }
  51. }
  52. export interface IDataParamSourceInBox extends IDataColumn {
  53. type: DropboxItemType
  54. visualType?: ViewModelVisualTypes
  55. chart?: IChartInfo
  56. config?: IDataParamConfig
  57. }
  58. interface IDropboxProps {
  59. name: string
  60. title: string
  61. type: DropboxType
  62. value: object
  63. items: IDataParamSource[]
  64. mode: WidgetMode
  65. selectedChartId: number
  66. dragged: IDataParamSource
  67. panelList: IDataParamSource[]
  68. dimetionsCount: number
  69. metricsCount: number
  70. onValueChange: (key: string, value: string) => void
  71. onItemDragStart: (item: IDataParamSource, e: React.DragEvent<HTMLLIElement | HTMLParagraphElement>) => void
  72. onItemDragEnd: (dropType: DropType) => void
  73. onItemRemove: (name: string) => (e) => void
  74. onItemSort: (item: IDataParamSource, sort: FieldSortTypes) => void
  75. onItemChangeAgg: (item: IDataParamSource, agg: AggregatorType) => void
  76. onItemChangeColorConfig: (item: IDataParamSource) => void
  77. onItemChangeFilterConfig: (item: IDataParamSource) => void
  78. onItemChangeFieldConfig: (item: IDataParamSource) => void
  79. onItemChangeFormatConfig: (item: IDataParamSource) => void
  80. onItemChangeChart: (item: IDataParamSource) => (chart: IChartInfo) => void
  81. beforeDrop: (name: string, cachedItem: IDataParamSource, resolve: (next: boolean) => void) => void
  82. onDrop: (name: string, dropIndex: number, dropType: DropType, changedItems: IDataParamSource[], config?: IDataParamConfig) => void
  83. }
  84. interface IDropboxStates {
  85. entering: boolean
  86. items: IDataParamSourceInBox[]
  87. dropIndex: number
  88. dropType: DropType
  89. }
  90. export class Dropbox extends React.PureComponent<IDropboxProps, IDropboxStates> {
  91. constructor (props) {
  92. super(props)
  93. this.state = {
  94. entering: false,
  95. items: [],
  96. dropIndex: -1,
  97. dropType: void 0
  98. }
  99. }
  100. private container: HTMLDivElement = null
  101. private width: number = 0
  102. private x: number = 0
  103. private y: number = 0
  104. private PADDING = 5
  105. private BOX_MIN_HEIGHT = 54
  106. private ITEM_HEIGHT = 28
  107. public componentWillMount () {
  108. this.getItems(this.props)
  109. }
  110. public componentWillReceiveProps (nextProps) {
  111. if (nextProps.items !== this.props.items) {
  112. this.getItems(nextProps)
  113. }
  114. }
  115. private getItems = (props) => {
  116. this.setState({
  117. items: [...props.items]
  118. })
  119. }
  120. private getBoxRect = () => {
  121. const rect = this.container.getBoundingClientRect() as DOMRect
  122. this.width = rect.width
  123. this.x = rect.x
  124. this.y = rect.y
  125. }
  126. private dragEnter = () => {
  127. this.getBoxRect()
  128. this.setState({
  129. dropIndex: 0,
  130. entering: true
  131. })
  132. }
  133. private dragOver = (e) => {
  134. e.preventDefault()
  135. const { items, dragged } = this.props
  136. if (!(dragged.type === 'category'
  137. && !dragged.from
  138. && items.find((i) => i.name === dragged.name))) {
  139. // if (this.props.size === 'large') {
  140. const { clientX, clientY } = e
  141. const physicalDropIndex = this.calcPhysicalDropIndex(clientX, clientY)
  142. this.previewDropPosition(physicalDropIndex)
  143. // } else {
  144. // if (this.state.dropIndex === -1) {
  145. // this.setState({
  146. // dropIndex: 0
  147. // })
  148. // }
  149. // }
  150. }
  151. }
  152. private dragLeave = () => {
  153. this.setState({
  154. items: this.state.items.filter((i) => i.type !== 'add'),
  155. entering: false,
  156. dropIndex: -1,
  157. dropType: void 0
  158. })
  159. }
  160. private drop = () => {
  161. const { name, items, dragged, beforeDrop, onDrop } = this.props
  162. const { items: itemsState, dropIndex, dropType } = this.state
  163. if (dropIndex >= 0) {
  164. const alreadyHaveIndex = items.findIndex((i) => i.name === dragged.name)
  165. if (!(dragged.type === 'category' && alreadyHaveIndex >= 0 && dragged.from !== name)) {
  166. beforeDrop(name, dragged, (data: boolean | IDataParamConfig) => {
  167. if (data) {
  168. onDrop(name, dropIndex, dropType, itemsState as IDataParamSource[], data as IDataParamConfig)
  169. } else {
  170. this.dragLeave()
  171. }
  172. })
  173. }
  174. }
  175. this.setState({
  176. entering: false,
  177. dropIndex: -1,
  178. dropType: dropType === 'outside'
  179. ? void 0
  180. : dropType === void 0
  181. ? 'unmoved'
  182. : dropType
  183. })
  184. }
  185. private itemDragEnd = () => {
  186. this.props.onItemDragEnd(this.state.dropType)
  187. this.setState({
  188. dropType: void 0
  189. })
  190. }
  191. private calcPhysicalDropIndex = (dragX, dragY): number => {
  192. const relX = dragX - this.x
  193. const relY = dragY - this.y
  194. const limitX = this.width - this.PADDING
  195. const limitY = Math.max(this.BOX_MIN_HEIGHT - this.PADDING, this.state.items.length * this.ITEM_HEIGHT + this.PADDING)
  196. if (relX > this.PADDING && relY > this.PADDING && relX < limitX && relY < limitY) {
  197. // const row = Math.floor((relX - this.PADDING) / this.ITEM_WIDTH)
  198. // const col = Math.floor((relY - this.PADDING) / this.ITEM_HEIGHT)
  199. // return col * 2 + row
  200. return Math.floor((relY - this.PADDING) / this.ITEM_HEIGHT)
  201. }
  202. }
  203. private previewDropPosition = (physicalDropIndex) => {
  204. const { items, dragged } = this.props
  205. const { items: itemsState } = this.state
  206. const draggedItemIndex = items.findIndex((i) => i.name === dragged.name)
  207. const draggedItemLocalIndex = itemsState.findIndex((is) => is.name === dragged.name)
  208. let itemLength = items.length
  209. if (draggedItemIndex >= 0) {
  210. itemLength -= 1
  211. }
  212. const realDropIndex = physicalDropIndex !== void 0
  213. ? Math.min(itemLength, physicalDropIndex)
  214. : itemLength
  215. if (draggedItemIndex < 0) {
  216. if (draggedItemLocalIndex < 0 || draggedItemLocalIndex !== realDropIndex) {
  217. this.setState({
  218. items: [
  219. ...items.slice(0, realDropIndex),
  220. {
  221. name: dragged.type === 'category' ? dragged.name : decodeMetricName(dragged.name),
  222. type: 'add'
  223. },
  224. ...items.slice(realDropIndex)
  225. ],
  226. dropIndex: realDropIndex,
  227. dropType: 'outside'
  228. })
  229. }
  230. } else {
  231. if (draggedItemLocalIndex !== realDropIndex) {
  232. const temp = itemsState.filter((i, index) => index !== draggedItemLocalIndex)
  233. temp.splice(realDropIndex, 0, dragged)
  234. this.setState({
  235. items: temp,
  236. dropIndex: realDropIndex,
  237. dropType: 'inside'
  238. })
  239. }
  240. }
  241. }
  242. public render () {
  243. const {
  244. name,
  245. title,
  246. type,
  247. value,
  248. panelList,
  249. mode,
  250. selectedChartId,
  251. dragged,
  252. dimetionsCount,
  253. metricsCount,
  254. onValueChange,
  255. onItemDragStart,
  256. onItemSort,
  257. onItemChangeAgg,
  258. onItemChangeColorConfig,
  259. onItemChangeFilterConfig,
  260. onItemChangeFieldConfig,
  261. onItemChangeFormatConfig,
  262. onItemChangeChart,
  263. onItemRemove
  264. } = this.props
  265. const { entering, items } = this.state
  266. let shouldResponse = false
  267. let shouleEnter = false
  268. let dragType = ''
  269. if (dragged) {
  270. dragType = dragged.type
  271. if (type === 'all' || type === dragType) {
  272. shouldResponse = true
  273. shouleEnter = entering
  274. }
  275. }
  276. const containerClass = classnames({
  277. [styles.dropContainer]: true,
  278. [styles.dragOver]: shouldResponse
  279. })
  280. const maskClass = classnames({
  281. [styles.mask]: true,
  282. [styles.onTop]: shouldResponse,
  283. [styles.enter]: shouleEnter,
  284. [styles.category]: dragType === 'category',
  285. [styles.value]: dragType === 'value'
  286. })
  287. let setting
  288. if (['color', 'size'].includes(name)) {
  289. let panel
  290. switch (name) {
  291. case 'color':
  292. panel = (
  293. <ColorPanel
  294. list={panelList}
  295. value={value}
  296. showAll={mode === 'pivot'}
  297. onValueChange={onValueChange}
  298. />
  299. )
  300. break
  301. case 'size':
  302. panel = (
  303. <SizePanel
  304. list={panelList}
  305. value={value}
  306. hasTabs={mode === 'pivot'}
  307. onValueChange={onValueChange}
  308. />
  309. )
  310. break
  311. }
  312. setting = (
  313. <Popover
  314. content={panel}
  315. trigger="click"
  316. placement="right"
  317. >
  318. <span className={styles.setting}>
  319. <Icon type="setting" /> 设置
  320. </span>
  321. </Popover>
  322. )
  323. }
  324. const itemContent = items.length
  325. ? items.map((item) => (
  326. <DropboxItem
  327. key={item.name}
  328. container={name}
  329. item={item}
  330. dimetionsCount={dimetionsCount}
  331. metricsCount={metricsCount}
  332. onDragStart={onItemDragStart}
  333. onDragEnd={this.itemDragEnd}
  334. onSort={onItemSort}
  335. onChangeAgg={onItemChangeAgg}
  336. onChangeFieldConfig={onItemChangeFieldConfig}
  337. onChangeFormatConfig={onItemChangeFormatConfig}
  338. onChangeColorConfig={onItemChangeColorConfig}
  339. onChangeFilterConfig={onItemChangeFilterConfig}
  340. onChangeChart={onItemChangeChart}
  341. onRemove={onItemRemove(item.name)}
  342. />
  343. ))
  344. : (
  345. <DropboxContent
  346. title={title}
  347. type={type}
  348. />
  349. )
  350. return (
  351. <div className={styles.dropbox}>
  352. <p className={styles.title}>
  353. {title}
  354. {setting}
  355. </p>
  356. <div
  357. className={containerClass}
  358. ref={(f) => this.container = f}
  359. >
  360. {itemContent}
  361. <div
  362. className={maskClass}
  363. onDragEnter={this.dragEnter}
  364. onDragOver={this.dragOver}
  365. onDragLeave={this.dragLeave}
  366. onDrop={this.drop}
  367. />
  368. </div>
  369. </div>
  370. )
  371. }
  372. }
  373. export default Dropbox