ComputedConfigForm.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  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 { FormComponentProps } from 'antd/lib/form'
  22. import { Form, Input, Select, Button, Row, Col, Menu, Tabs } from 'antd'
  23. const FormItem = Form.Item
  24. const Option = Select.Option
  25. const Search = Input.Search
  26. const TabPane = Tabs.TabPane
  27. import { uuid } from 'utils/util'
  28. const codeMirror = require('codemirror/lib/codemirror')
  29. const utilStyles = require('assets/less/util.less')
  30. import sqlFunctions from 'assets/sqlFunctionName/sqlFns'
  31. import 'codemirror/lib/codemirror.css'
  32. import 'assets/override/codemirror_theme.css'
  33. require('codemirror/mode/javascript/javascript')
  34. import 'codemirror/addon/lint/lint.css'
  35. require('codemirror/addon/hint/javascript-hint')
  36. require('codemirror/addon/lint/json-lint')
  37. require('codemirror/addon/lint/css-lint')
  38. require('codemirror/addon/lint/lint')
  39. require('codemirror/addon/lint/javascript-lint')
  40. require('codemirror/addon/lint/json-lint')
  41. require('codemirror/addon/lint/css-lint')
  42. const styles = require('../Widget.less')
  43. interface IComputedConfigFormProps {
  44. form: any,
  45. onSave: (obj: any) => void,
  46. onClose: () => void,
  47. categories: any,
  48. queryInfo: any
  49. selectedComputed: object
  50. }
  51. interface IComputedConfigFormStates {
  52. variableNumber: number
  53. categories: any
  54. filterFunction: string
  55. queryInfo: string[]
  56. }
  57. export class ComputedConfigForm extends React.Component<IComputedConfigFormProps & FormComponentProps, IComputedConfigFormStates> {
  58. private Editor: any
  59. constructor(props) {
  60. super(props)
  61. this.state = {
  62. variableNumber: 1,
  63. queryInfo: [],
  64. categories: [],
  65. filterFunction: ''
  66. }
  67. this.Editor = false
  68. }
  69. public componentWillMount() {
  70. const { queryInfo, categories } = this.props
  71. this.setState({
  72. queryInfo,
  73. categories
  74. })
  75. }
  76. public componentDidMount() {
  77. const queryTextarea = document.querySelector('#sql_tmpl')
  78. this.handleTmplCodeMirror(queryTextarea)
  79. }
  80. public componentWillReceiveProps(nextProps) {
  81. const { selectedComputed } = nextProps
  82. if (selectedComputed && selectedComputed.sqlExpression) {
  83. this.Editor.doc.setValue(selectedComputed.sqlExpression)
  84. } else {
  85. this.Editor.doc.setValue('')
  86. }
  87. }
  88. private customDvSqlMode = () => {
  89. const { categories, queryInfo } = this.props
  90. const highLightFields = categories.map((cate) => `${cate.name}`)
  91. const hightLightQuery = [...queryInfo]
  92. codeMirror.defineMode('dvSqlMode', (e, t) => {
  93. function r(t, r) {
  94. const o = t.next()
  95. if (p[o]) {
  96. const i = p[o](t, r, '{' === o ? hightLightQuery : highLightFields)
  97. if (i !== !1) {
  98. return i
  99. }
  100. }
  101. if ((1 == m.nCharCast && ("n" == o || "N" == o) || 1 == m.charsetCast && "_" == o && t.match(/[a-z][a-z0-9]*/i)) && ("'" == t.peek() || '"' == t.peek()))
  102. return "keyword";
  103. if (/^[\(\),\;\[\]\{\}]/.test(o))
  104. return null;
  105. if ("." != o) {
  106. if (d.test(o))
  107. return t.eatWhile(d),
  108. "operator";
  109. if ("{" == o && (t.match(/^( )*(d|D|t|T|ts|TS)( )*'[^']*'( )*}/) || t.match(/^( )*(d|D|t|T|ts|TS)( )*"[^"]*"( )*}/)))
  110. return "number";
  111. t.eatWhile(/^[_\w\d]/);
  112. const h = t.current().toLowerCase();
  113. return f.hasOwnProperty(h) && (t.match(/^( )+'[^']*'/) || t.match(/^( )+"[^"]*"/)) ? "number" : l.hasOwnProperty(h) ? "atom" : u.hasOwnProperty(h) ? "keyword" : c.hasOwnProperty(h) ? "builtin" : s.hasOwnProperty(h) ? "string-2" : null
  114. }
  115. }
  116. function o(e, t, r) {
  117. t.context = {
  118. prev: t.context,
  119. indent: e.indentation(),
  120. col: e.column(),
  121. type: r
  122. }
  123. }
  124. function i(e) {
  125. e.indent = e.context.indent,
  126. e.context = e.context.prev
  127. }
  128. const s = t.client || {}
  129. const l = t.atoms || {
  130. false: !0,
  131. true: !0,
  132. null: !0
  133. }
  134. const c = t.builtin || {}
  135. const u = t.keywords || {}
  136. const d = t.operatorChars || /^[\/\*\+\-%<>!=&|~^]/
  137. const m = t.support || {}
  138. const p = t.hooks || {}
  139. const f = t.dateSQL || {
  140. date: !0,
  141. time: !0,
  142. timestamp: !0
  143. }
  144. return {
  145. startState: () => {
  146. return {
  147. tokenize: r,
  148. context: null
  149. }
  150. },
  151. token: (e, t) => {
  152. if (e.sol() && t.context && null == t.context.align && (t.context.align = !1),
  153. e.eatSpace()) {
  154. return null
  155. }
  156. const r = t.tokenize(e, t)
  157. if ('comment' === r) {
  158. return r
  159. }
  160. t.context && null == t.context.align && (t.context.align = !0)
  161. const n = e.current()
  162. return "(" == n ? o(e, t, ")") : "[" == n ? o(e, t, "]") : t.context && t.context.type == n && i(t),
  163. r
  164. }
  165. }
  166. })
  167. function n(e, t, r) {
  168. for (var n, a = ""; null != (n = e.next());) {
  169. if ("]" == n && !e.eat("]"))
  170. return r && r.indexOf(a) < 0 ? "error" : "variable-2";
  171. a += n
  172. }
  173. return null
  174. }
  175. function a(e, t, r) {
  176. for (var n, a = ""; null != (n = e.next());) {
  177. if ("}" == n && !e.eat("}"))
  178. return r && r.indexOf(a) < 0 ? "error" : "variable-2";
  179. a += n
  180. }
  181. return null
  182. }
  183. function s(e) {
  184. for (var t = {}, r = e.split(" "), n = 0; n < r.length; ++n)
  185. t[r[n]] = !0;
  186. return t
  187. }
  188. const l = 'sum avg count max min median stddev stdev_pop stddev_samp var_pop var_samp variance percentiles percentile_cont percentile_disc'
  189. codeMirror.defineMIME('text/x-dv-sql-mode', {
  190. name: 'dvSqlMode',
  191. keywords: s(l),
  192. hooks: {
  193. '[': n,
  194. '{': a
  195. }
  196. })
  197. const hintList = sqlFunctions.map((fn) => fn.name).concat(categories.map((cate) => cate.name)).concat(queryInfo)
  198. codeMirror.registerHelper('hint', 'dvSqlMode', (cm) => {
  199. const cur = cm.getCursor()
  200. const token = cm.getTokenAt(cur)
  201. const start = token.start
  202. const end = cur.ch
  203. const str = token.string
  204. const list = hintList.filter((item) => {
  205. return item.indexOf(str) === 0
  206. })
  207. if (list.length) {
  208. return {
  209. list,
  210. from: codeMirror.Pos(cur.line, start),
  211. to: codeMirror.Pos(cur.line, end)
  212. }
  213. }
  214. })
  215. }
  216. private handleTmplCodeMirror = (queryWrapperDOM) => {
  217. this.customDvSqlMode()
  218. if (!this.Editor) {
  219. this.Editor = codeMirror.fromTextArea(queryWrapperDOM, {
  220. mode: 'text/x-dv-sql-mode',
  221. theme: '3024-day',
  222. width: '100%',
  223. height: '80px',
  224. lineNumbers: true,
  225. lineWrapping: true,
  226. autoCloseBrackets: true,
  227. matchBrackets: true,
  228. foldGutter: true,
  229. gutters: ['CodeMirror-lint-markers', 'CodeMirror-linenumbers', 'CodeMirror-foldgutter']
  230. })
  231. this.Editor.addKeyMap({
  232. 'name': 'autoInsertParentheses',
  233. "'('": (cm) => {
  234. const cur = cm.getCursor()
  235. cm.replaceRange('()', cur, cur, '+insert')
  236. cm.doc.setCursor({ line: cur.line, ch: cur.ch + 1 })
  237. },
  238. "'['": (cm) => {
  239. const cur = cm.getCursor()
  240. cm.replaceRange('[]', cur, cur, '+insert')
  241. cm.doc.setCursor({ line: cur.line, ch: cur.ch + 1 })
  242. },
  243. "'{'": (cm) => {
  244. const cur = cm.getCursor()
  245. cm.replaceRange('{}', cur, cur, '+insert')
  246. cm.doc.setCursor({ line: cur.line, ch: cur.ch + 1 })
  247. }
  248. })
  249. this.Editor.on('change', function (editor, change) {
  250. if (change.origin === '+input') {
  251. const text = change.text
  252. setTimeout(function () { editor.execCommand('autocomplete') }, 50)
  253. }
  254. })
  255. }
  256. }
  257. private saveComputed = () => {
  258. this.props.form.validateFieldsAndScroll((err, values) => {
  259. if (!err) {
  260. const { selectedComputed } = this.props
  261. const sqlExpression = this.Editor.doc.getValue()
  262. const { id, name, visualType } = this.props.form.getFieldsValue()
  263. console.log({ id })
  264. console.log({ selectedComputed })
  265. this.props.onSave({
  266. id: id ? id : uuid(8, 16),
  267. name,
  268. visualType,
  269. sqlExpression,
  270. title: 'computedField',
  271. from: selectedComputed ? selectedComputed['from'] : ''
  272. })
  273. this.closePanel()
  274. }
  275. })
  276. }
  277. private closePanel = () => {
  278. this.resetForm()
  279. this.props.onClose()
  280. }
  281. private resetForm = () => {
  282. this.Editor.doc.setValue('')
  283. this.props.form.resetFields()
  284. }
  285. private filterFunction = (value) => {
  286. this.setState({
  287. filterFunction: value
  288. })
  289. }
  290. private filterModel = (value) => {
  291. this.setState({
  292. categories: this.props.categories.filter((cate) => {
  293. return cate.name.indexOf(value) >= 0
  294. })
  295. })
  296. }
  297. private filterQuery = (value) => {
  298. this.setState({
  299. queryInfo: this.props.queryInfo.filter((query) => {
  300. return query.indexOf(value) >= 0
  301. })
  302. })
  303. }
  304. private triggerMenuItem = (params) => ({ item, key, keyPath }) => {
  305. const defaultValue = this.Editor.doc.getValue()
  306. let currentValue = ''
  307. switch (params) {
  308. case 'fn':
  309. currentValue = `${defaultValue}${key}()`
  310. break
  311. case 'category':
  312. currentValue = `${defaultValue}[${key}]`
  313. break
  314. case 'query':
  315. currentValue = `${defaultValue}{${key}}`
  316. break
  317. default:
  318. break
  319. }
  320. this.Editor.doc.setValue(currentValue)
  321. }
  322. public render() {
  323. const {
  324. form
  325. } = this.props
  326. const {
  327. queryInfo,
  328. categories,
  329. variableNumber,
  330. filterFunction
  331. } = this.state
  332. const { getFieldDecorator } = form
  333. const controlTypeOptions = [
  334. { text: '文本', value: 'string' },
  335. { text: '数值', value: 'number' },
  336. { text: '日期', value: 'date' }
  337. ].map((o) => (
  338. <Option key={o.value} value={o.value}>{o.text}</Option>
  339. ))
  340. const functions = sqlFunctions.filter((func) => {
  341. return func.name.indexOf(filterFunction.toUpperCase()) >= 0
  342. })
  343. const functionSelectMenu = (
  344. <Menu onClick={this.triggerMenuItem('fn')}>
  345. {functions && functions.length ? functions.map((d, index) =>
  346. <Menu.Item key={`${d.name}`}>
  347. <a target="_blank" rel="noopener noreferrer" href="javascript:;">{d.name}</a>
  348. </Menu.Item>
  349. ) : []}
  350. </Menu>
  351. )
  352. const modelSelectMenu = (
  353. <Menu onClick={this.triggerMenuItem('category')}>
  354. {categories && categories.length ? categories.map((d, index) =>
  355. <Menu.Item key={`${d.name}`}>
  356. <a target="_blank" rel="noopener noreferrer" href="javascript:;">{d.name}</a>
  357. </Menu.Item>
  358. ) : []}
  359. </Menu>
  360. )
  361. const querySelectMenu = (
  362. <Menu onClick={this.triggerMenuItem('query')}>
  363. {queryInfo && queryInfo.length ? queryInfo.map((query, index) =>
  364. <Menu.Item key={`${query}`}>
  365. <a target="_blank" rel="noopener noreferrer" href="javascript:;">{query}</a>
  366. </Menu.Item>
  367. ) : []}
  368. </Menu>
  369. )
  370. return (
  371. <div className={styles.computedConfigForm}>
  372. <div className={styles.body}>
  373. <div className={styles.fields}>
  374. <div className={styles.fieldName}>
  375. <Form>
  376. <Row gutter={8}>
  377. <Col span={12}>
  378. <FormItem className={utilStyles.hide}>
  379. {getFieldDecorator('id', {})(
  380. <Input />
  381. )}
  382. </FormItem>
  383. <FormItem>
  384. {getFieldDecorator('name', {
  385. rules: [{
  386. required: true,
  387. message: '计算字段名称不能为空'
  388. }]
  389. })(
  390. <Input placeholder="计算字段名称" />
  391. )}
  392. </FormItem>
  393. </Col>
  394. <Col span={12} key="visualType">
  395. <FormItem>
  396. {getFieldDecorator('visualType', {
  397. rules: [{
  398. required: true,
  399. message: '计算字段类型不能为空'
  400. }]
  401. })(
  402. <Select placeholder="计算字段类型">
  403. {controlTypeOptions}
  404. </Select>
  405. )}
  406. </FormItem>
  407. </Col>
  408. </Row>
  409. </Form>
  410. </div>
  411. <div className={styles.tmplWrapper}>
  412. <textarea id="sql_tmpl" placeholder="输入SQL语句" />
  413. </div>
  414. </div>
  415. <div className={styles.options}>
  416. <div className={styles.cardContainer}>
  417. <Tabs defaultActiveKey="functions" size="small" tabPosition="right">
  418. <TabPane tab="函数" key="functions">
  419. <div className={styles.menuWrapper}>
  420. <Search
  421. placeholder="请选择"
  422. onSearch={this.filterFunction}
  423. />
  424. {functionSelectMenu}
  425. </div>
  426. </TabPane>
  427. <TabPane tab="字段" key="model">
  428. <div className={styles.menuWrapper}>
  429. <Search
  430. placeholder="请选择"
  431. onSearch={this.filterModel}
  432. />
  433. {modelSelectMenu}
  434. </div>
  435. </TabPane>
  436. <TabPane tab="变量" key="query">
  437. <div className={styles.menuWrapper}>
  438. <Search
  439. placeholder="请选择"
  440. onSearch={this.filterQuery}
  441. />
  442. {querySelectMenu}
  443. </div>
  444. </TabPane>
  445. </Tabs>
  446. </div>
  447. </div>
  448. </div>
  449. <div className={styles.footer}>
  450. <div className={styles.foot}>
  451. <Button onClick={this.closePanel}>取消</Button>
  452. <Button type="primary" onClick={this.saveComputed}>保存</Button>
  453. </div>
  454. </div>
  455. </div>
  456. )
  457. }
  458. }
  459. export default Form.create<IComputedConfigFormProps & FormComponentProps>()(ComputedConfigForm)