Editor.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  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 injectReducer from 'utils/injectReducer'
  27. import injectSaga from 'utils/injectSaga'
  28. import reducer from './reducer'
  29. import sagas from './sagas'
  30. import reducerSource from 'containers/Source/reducer'
  31. import sagasSource from 'containers/Source/sagas'
  32. import reducerProject from 'containers/Projects/reducer'
  33. import sagasProject from 'containers/Projects/sagas'
  34. import { RouteComponentWithParams } from 'utils/types'
  35. import { hideNavigator } from '../App/actions'
  36. import { ViewActions, ViewActionType } from './actions'
  37. import { SourceActions, SourceActionType } from 'containers/Source/actions'
  38. import {
  39. OrganizationActions,
  40. OrganizationActionType
  41. } from 'containers/Organizations/actions'
  42. import {
  43. makeSelectEditingView,
  44. makeSelectEditingViewInfo,
  45. makeSelectSources,
  46. makeSelectSchema,
  47. makeSelectSqlDataSource,
  48. makeSelectSqlLimit,
  49. makeSelectSqlValidation,
  50. makeSelectLoading,
  51. makeSelectChannels,
  52. makeSelectTenants,
  53. makeSelectBizs,
  54. makeSelectIsLastExecuteWholeSql
  55. } from './selectors'
  56. import { makeSelectProjectRoles } from 'containers/Projects/selectors'
  57. import {
  58. IView,
  59. IViewModel,
  60. IViewRoleRaw,
  61. IViewRole,
  62. IViewVariable,
  63. IViewInfo,
  64. IExecuteSqlParams,
  65. IExecuteSqlResponse,
  66. IViewLoading,
  67. ISqlValidation,
  68. IDacChannel,
  69. IDacTenant,
  70. IDacBiz
  71. } from './types'
  72. import { ISource, ISchema } from '../Source/types'
  73. import { ViewVariableTypes } from './constants'
  74. import { message, notification, Tooltip } from 'antd'
  75. import EditorSteps from './components/EditorSteps'
  76. import EditorContainer from './components/EditorContainer'
  77. import ModelAuth from './components/ModelAuth'
  78. import SourceTable from './components/SourceTable'
  79. import SqlEditor from './components/SqlEditorByAce'
  80. import SqlPreview from './components/SqlPreview'
  81. import EditorBottom from './components/EditorBottom'
  82. import ViewVariableList from './components/ViewVariableList'
  83. import VariableModal from './components/VariableModal'
  84. import Styles from './View.less'
  85. import { querystring } from '../../../share/util'
  86. interface IViewEditorStateProps {
  87. editingView: IView
  88. editingViewInfo: IViewInfo
  89. sources: ISource[]
  90. schema: ISchema
  91. sqlDataSource: IExecuteSqlResponse
  92. sqlLimit: number
  93. sqlValidation: ISqlValidation
  94. loading: IViewLoading
  95. projectRoles: any[]
  96. channels: IDacChannel[]
  97. tenants: IDacTenant[]
  98. bizs: IDacBiz[]
  99. isLastExecuteWholeSql: boolean
  100. }
  101. export enum EExecuteType {
  102. whole,
  103. single
  104. }
  105. interface IViewEditorDispatchProps {
  106. onHideNavigator: () => void
  107. onLoadViewDetail: (viewId: number) => void
  108. onLoadSources: (projectId: number) => void
  109. onLoadSourceDatabases: (sourceId: number) => void
  110. onLoadDatabaseTables: (sourceId: number, databaseName: string) => void
  111. onLoadTableColumns: (
  112. sourceId: number,
  113. databaseName: string,
  114. tableName: string
  115. ) => void
  116. onExecuteSql: (params: IExecuteSqlParams, exeType: EExecuteType) => void
  117. onAddView: (view: IView, resolve: () => void) => void
  118. onEditView: (view: IView, resolve: () => void) => void
  119. onUpdateEditingView: (view: IView) => void
  120. onUpdateEditingViewInfo: (viewInfo: IViewInfo) => void
  121. onSetSqlLimit: (limit: number) => void
  122. onLoadDacChannels: () => void
  123. onLoadDacTenants: (channelName: string) => void
  124. onLoadDacBizs: (channelName: string, tenantId: number) => void
  125. onResetState: () => void
  126. onLoadProjectRoles: (projectId: number) => void
  127. onSetIsLastExecuteWholeSql: (isLastExecuteWholeSql: boolean) => void
  128. }
  129. type IViewEditorProps = IViewEditorStateProps &
  130. IViewEditorDispatchProps &
  131. RouteComponentWithParams
  132. interface IViewEditorStates {
  133. containerHeight: number
  134. sqlValidationCode: number
  135. init: boolean
  136. currentStep: number
  137. lastSuccessExecutedSql: string
  138. sqlFragment: string
  139. }
  140. export class ViewEditor extends React.Component<
  141. IViewEditorProps,
  142. IViewEditorStates
  143. > {
  144. public state: Readonly<IViewEditorStates> = {
  145. containerHeight: 0,
  146. currentStep: 0,
  147. sqlValidationCode: null,
  148. init: true,
  149. lastSuccessExecutedSql: null,
  150. sqlFragment: ''
  151. }
  152. public constructor(props: IViewEditorProps) {
  153. super(props)
  154. const {
  155. onHideNavigator,
  156. onLoadSources,
  157. onLoadViewDetail,
  158. onLoadProjectRoles,
  159. onLoadDacChannels,
  160. match
  161. } = this.props
  162. onHideNavigator()
  163. const { viewId, projectId } = match.params
  164. if (projectId) {
  165. onLoadSources(+projectId)
  166. onLoadProjectRoles(+projectId)
  167. }
  168. if (viewId) {
  169. onLoadViewDetail(+viewId)
  170. }
  171. onLoadDacChannels()
  172. }
  173. public static getDerivedStateFromProps: React.GetDerivedStateFromProps<
  174. IViewEditorProps,
  175. IViewEditorStates
  176. > = (props, state) => {
  177. const { match, editingView, sqlValidation } = props
  178. const { viewId } = match.params
  179. const { init, sqlValidationCode } = state
  180. let { lastSuccessExecutedSql } = state
  181. if (sqlValidationCode !== sqlValidation.code && sqlValidation.code) {
  182. notification.destroy()
  183. sqlValidation.code === 200
  184. ? notification.success({
  185. message: '执行成功',
  186. duration: 3
  187. })
  188. : notification.error({
  189. message: '执行失败',
  190. description: (
  191. <Tooltip
  192. placement="bottom"
  193. trigger="click"
  194. title={sqlValidation.message}
  195. overlayClassName={Styles.errorMessage}
  196. >
  197. <a>点击查看错误信息</a>
  198. </Tooltip>
  199. ),
  200. duration: null
  201. })
  202. if (sqlValidation.code === 200) {
  203. lastSuccessExecutedSql = editingView.sql
  204. }
  205. }
  206. if (editingView && editingView.id === +viewId) {
  207. if (init) {
  208. props.onLoadSourceDatabases(editingView.sourceId)
  209. lastSuccessExecutedSql = editingView.sql
  210. return {
  211. init: false,
  212. sqlValidationCode: sqlValidation.code,
  213. lastSuccessExecutedSql
  214. }
  215. }
  216. }
  217. return { sqlValidationCode: sqlValidation.code, lastSuccessExecutedSql }
  218. }
  219. public componentWillUnmount() {
  220. this.props.onResetState()
  221. notification.destroy()
  222. }
  223. private executeSql = () => {
  224. const { sqlFragment } = this.state
  225. const { onSetIsLastExecuteWholeSql } = this.props
  226. if (sqlFragment != null) {
  227. onSetIsLastExecuteWholeSql(false)
  228. }
  229. ViewEditor.ExecuteSql(this.props, this.state.sqlFragment)
  230. }
  231. private static ExecuteSql = (
  232. props: IViewEditorProps,
  233. sqlFragment?: string
  234. ) => {
  235. const { onExecuteSql, editingView, editingViewInfo, sqlLimit } = props
  236. const { sourceId, sql } = editingView
  237. const { variable } = editingViewInfo
  238. const updatedParams: IExecuteSqlParams = {
  239. sourceId,
  240. sql: sqlFragment ?? sql,
  241. limit: sqlLimit,
  242. variables: variable
  243. }
  244. const exeType =
  245. sqlFragment == null ? EExecuteType.whole : EExecuteType.single
  246. onExecuteSql(updatedParams, exeType)
  247. }
  248. private stepChange = (step: number) => {
  249. const { currentStep } = this.state
  250. if (currentStep + step < 0) {
  251. this.goToViewList()
  252. return
  253. }
  254. const { editingView } = this.props
  255. const { name, sourceId, sql } = editingView
  256. const errorMessages = ['名称不能为空', '请选择数据源', 'sql 不能为空']
  257. const fieldsValue = [name, sourceId, sql]
  258. const hasError = fieldsValue.some((val, idx) => {
  259. if (!val) {
  260. message.error(errorMessages[idx])
  261. return true
  262. }
  263. })
  264. if (hasError) {
  265. return
  266. }
  267. this.setState({ currentStep: currentStep + step }, () => {
  268. if (this.state.currentStep > 1) {
  269. this.saveView()
  270. }
  271. })
  272. }
  273. private saveView = () => {
  274. const searchParams = querystring(
  275. this.props.location?.search?.replace('?', '')
  276. )
  277. const {
  278. onAddView,
  279. onEditView,
  280. editingView,
  281. editingViewInfo,
  282. projectRoles,
  283. match
  284. } = this.props
  285. const { projectId } = match.params
  286. const { model, variable, roles } = editingViewInfo
  287. const { id: viewId } = editingView
  288. const validRoles = roles.filter(
  289. ({ roleId }) =>
  290. projectRoles && projectRoles.findIndex(({ id }) => id === roleId) >= 0
  291. )
  292. const updatedView: IView = {
  293. ...editingView,
  294. projectId: +projectId,
  295. // @ts-ignore
  296. parentId: searchParams?.parentId * 1 || null,
  297. model: JSON.stringify(model),
  298. variable: JSON.stringify(variable),
  299. roles: validRoles.map<IViewRoleRaw>(({ roleId, columnAuth, rowAuth }) => {
  300. const validColumnAuth = columnAuth.filter((c) => !!model[c])
  301. const validRowAuth = rowAuth.filter((r) => {
  302. const v = variable.find((v) => v.name === r.name)
  303. if (!v) {
  304. return false
  305. }
  306. return v.type === ViewVariableTypes.Authorization && !v.fromService
  307. })
  308. return {
  309. roleId,
  310. columnAuth: JSON.stringify(validColumnAuth),
  311. rowAuth: JSON.stringify(validRowAuth)
  312. }
  313. })
  314. }
  315. viewId
  316. ? onEditView(updatedView, this.goToViewList)
  317. : onAddView(updatedView, this.goToViewList)
  318. }
  319. private goToViewList = () => {
  320. const { history, match } = this.props
  321. const { projectId } = match.params
  322. const prefix = window.localStorage.getItem('inDataService') ?? ''
  323. const prefixPath = prefix ? '/' + prefix : prefix
  324. history.replace(`/project/${projectId}${prefixPath}/views`)
  325. window.location.reload()
  326. }
  327. private viewChange = (propName: keyof IView, value: string | number) => {
  328. const { editingView, onUpdateEditingView } = this.props
  329. const updatedView = {
  330. ...editingView,
  331. [propName]: value
  332. }
  333. onUpdateEditingView(updatedView)
  334. }
  335. private sqlChange = (sql: string) => {
  336. this.viewChange('sql', sql)
  337. }
  338. private sqlSelect = (sqlFragment: string) => {
  339. this.setState({ sqlFragment })
  340. }
  341. private modelChange = (partialModel: IViewModel) => {
  342. const { editingViewInfo, onUpdateEditingViewInfo } = this.props
  343. const { model } = editingViewInfo
  344. const updatedViewInfo: IViewInfo = {
  345. ...editingViewInfo,
  346. model: { ...model, ...partialModel }
  347. }
  348. onUpdateEditingViewInfo(updatedViewInfo)
  349. }
  350. private variableChange = (updatedVariable: IViewVariable[]) => {
  351. const { editingViewInfo, onUpdateEditingViewInfo } = this.props
  352. const updatedViewInfo: IViewInfo = {
  353. ...editingViewInfo,
  354. variable: updatedVariable
  355. }
  356. onUpdateEditingViewInfo(updatedViewInfo)
  357. }
  358. /**
  359. * 数组长度1为单选,大于1为全选
  360. * @param {IViewRole[]} viewRoles
  361. * @private
  362. * @memberof ViewEditor
  363. */
  364. private viewRoleChange = (viewRoles: IViewRole[]) => {
  365. const { editingViewInfo, onUpdateEditingViewInfo } = this.props
  366. let updatedRoles: IViewRole[] = []
  367. if (viewRoles.length === 1) {
  368. const [viewRole] = viewRoles
  369. const { roles } = editingViewInfo
  370. updatedRoles = roles.filter((role) => role.roleId !== viewRole.roleId)
  371. updatedRoles.push(viewRole)
  372. } else {
  373. updatedRoles = viewRoles
  374. }
  375. const updatedViewInfo = {
  376. ...editingViewInfo,
  377. roles: updatedRoles
  378. }
  379. onUpdateEditingViewInfo(updatedViewInfo)
  380. }
  381. private getSqlHints = memoizeOne(
  382. (sourceId: number, schema: ISchema, variables: IViewVariable[]) => {
  383. if (!sourceId) {
  384. return {}
  385. }
  386. const variableHints = variables.reduce((acc, v) => {
  387. acc[`$${v.name}$`] = []
  388. return acc
  389. }, {})
  390. const { mapDatabases, mapTables, mapColumns } = schema
  391. if (!mapDatabases[sourceId]) {
  392. return {}
  393. }
  394. const tableHints: { [tableName: string]: string[] } = Object.values(
  395. mapTables
  396. ).reduce((acc, tablesInfo) => {
  397. if (tablesInfo.sourceId !== sourceId) {
  398. return acc
  399. }
  400. tablesInfo.tables.forEach(({ name: tableName }) => {
  401. acc[tableName] = []
  402. })
  403. return acc
  404. }, {})
  405. Object.values(mapColumns).forEach((columnsInfo) => {
  406. if (columnsInfo.sourceId !== sourceId) {
  407. return
  408. }
  409. const { tableName, columns } = columnsInfo
  410. if (tableHints[tableName]) {
  411. tableHints[tableName] = tableHints[tableName].concat(
  412. columns.map((col) => col.name)
  413. )
  414. }
  415. })
  416. const hints = {
  417. ...variableHints,
  418. ...tableHints
  419. }
  420. return hints
  421. }
  422. )
  423. public render() {
  424. const {
  425. sources,
  426. schema,
  427. sqlDataSource,
  428. sqlLimit,
  429. loading,
  430. projectRoles,
  431. channels,
  432. tenants,
  433. bizs,
  434. editingView,
  435. editingViewInfo,
  436. isLastExecuteWholeSql,
  437. onLoadSourceDatabases,
  438. onLoadDatabaseTables,
  439. onLoadTableColumns,
  440. onSetSqlLimit,
  441. onLoadDacTenants,
  442. onLoadDacBizs
  443. } = this.props
  444. const { currentStep, lastSuccessExecutedSql, sqlFragment } = this.state
  445. const { model, variable, roles: viewRoles } = editingViewInfo
  446. const sqlHints = this.getSqlHints(editingView.sourceId, schema, variable)
  447. const containerVisible = !currentStep
  448. const modelAuthVisible = !!currentStep
  449. const nextDisabled = editingView.sql !== lastSuccessExecutedSql
  450. return (
  451. <>
  452. <Helmet title="数据资产" />
  453. <div className={Styles.viewEditor}>
  454. <div className={Styles.header}>
  455. <div className={Styles.steps}>
  456. <EditorSteps current={currentStep} />
  457. </div>
  458. </div>
  459. <EditorContainer
  460. visible={containerVisible}
  461. variable={variable}
  462. onVariableChange={this.variableChange}
  463. >
  464. <SourceTable
  465. key="SourceTable"
  466. view={editingView}
  467. sources={sources}
  468. schema={schema}
  469. onViewChange={this.viewChange}
  470. onSourceSelect={onLoadSourceDatabases}
  471. onDatabaseSelect={onLoadDatabaseTables}
  472. onTableSelect={onLoadTableColumns}
  473. />
  474. <SqlEditor
  475. key="SqlEditor"
  476. value={editingView.sql}
  477. hints={sqlHints}
  478. onSqlChange={this.sqlChange}
  479. onSelect={this.sqlSelect}
  480. onCmdEnter={this.executeSql}
  481. />
  482. <SqlPreview
  483. key="SqlPreview"
  484. size="small"
  485. loading={loading.execute}
  486. response={sqlDataSource}
  487. />
  488. <EditorBottom
  489. key="EditorBottom"
  490. sqlLimit={sqlLimit}
  491. loading={loading.execute}
  492. nextDisabled={nextDisabled}
  493. sqlFragment={sqlFragment}
  494. isLastExecuteWholeSql={isLastExecuteWholeSql}
  495. onSetSqlLimit={onSetSqlLimit}
  496. onExecuteSql={this.executeSql}
  497. onStepChange={this.stepChange}
  498. />
  499. <ViewVariableList key="ViewVariableList" variables={variable} />
  500. <VariableModal
  501. key="VariableModal"
  502. channels={channels}
  503. tenants={tenants}
  504. bizs={bizs}
  505. onLoadDacTenants={onLoadDacTenants}
  506. onLoadDacBizs={onLoadDacBizs}
  507. />
  508. </EditorContainer>
  509. <ModelAuth
  510. visible={modelAuthVisible}
  511. model={model}
  512. variable={variable}
  513. sqlColumns={sqlDataSource.columns}
  514. roles={projectRoles}
  515. viewRoles={viewRoles}
  516. onModelChange={this.modelChange}
  517. onViewRoleChange={this.viewRoleChange}
  518. onStepChange={this.stepChange}
  519. />
  520. </div>
  521. </>
  522. )
  523. }
  524. }
  525. const mapDispatchToProps = (dispatch) => ({
  526. onHideNavigator: () => dispatch(hideNavigator()),
  527. onLoadViewDetail: (viewId: number) =>
  528. dispatch(ViewActions.loadViewsDetail([viewId], null, true)),
  529. onLoadSources: (projectId) => dispatch(SourceActions.loadSources(projectId)),
  530. onLoadSourceDatabases: (sourceId) =>
  531. dispatch(SourceActions.loadSourceDatabases(sourceId)),
  532. onLoadDatabaseTables: (sourceId, databaseName) =>
  533. dispatch(SourceActions.loadDatabaseTables(sourceId, databaseName)),
  534. onLoadTableColumns: (sourceId, databaseName, tableName) =>
  535. dispatch(SourceActions.loadTableColumns(sourceId, databaseName, tableName)),
  536. onExecuteSql: (params, exeType?) =>
  537. dispatch(ViewActions.executeSql(params, exeType)),
  538. onSetIsLastExecuteWholeSql: (isLastExecuteWholeSql: boolean) =>
  539. dispatch(ViewActions.setIsLastExecuteWholeSql(isLastExecuteWholeSql)),
  540. onAddView: (view, resolve) => dispatch(ViewActions.addView(view, resolve)),
  541. onEditView: (view, resolve) => dispatch(ViewActions.editView(view, resolve)),
  542. onUpdateEditingView: (view) => dispatch(ViewActions.updateEditingView(view)),
  543. onUpdateEditingViewInfo: (viewInfo: IViewInfo) =>
  544. dispatch(ViewActions.updateEditingViewInfo(viewInfo)),
  545. onSetSqlLimit: (limit: number) => dispatch(ViewActions.setSqlLimit(limit)),
  546. onLoadDacChannels: () => dispatch(ViewActions.loadDacChannels()),
  547. onLoadDacTenants: (channelName) =>
  548. dispatch(ViewActions.loadDacTenants(channelName)),
  549. onLoadDacBizs: (channelName, tenantId) =>
  550. dispatch(ViewActions.loadDacBizs(channelName, tenantId)),
  551. onResetState: () => dispatch(ViewActions.resetViewState()),
  552. onLoadProjectRoles: (projectId) =>
  553. dispatch(OrganizationActions.loadProjectRoles(projectId))
  554. })
  555. const mapStateToProps = createStructuredSelector({
  556. editingView: makeSelectEditingView(),
  557. editingViewInfo: makeSelectEditingViewInfo(),
  558. sources: makeSelectSources(),
  559. schema: makeSelectSchema(),
  560. sqlDataSource: makeSelectSqlDataSource(),
  561. sqlLimit: makeSelectSqlLimit(),
  562. sqlValidation: makeSelectSqlValidation(),
  563. loading: makeSelectLoading(),
  564. projectRoles: makeSelectProjectRoles(),
  565. channels: makeSelectChannels(),
  566. tenants: makeSelectTenants(),
  567. bizs: makeSelectBizs(),
  568. isLastExecuteWholeSql: makeSelectIsLastExecuteWholeSql()
  569. })
  570. const withConnect = connect(mapStateToProps, mapDispatchToProps)
  571. const withReducer = injectReducer({ key: 'view', reducer })
  572. const withSaga = injectSaga({ key: 'view', saga: sagas })
  573. const withReducerSource = injectReducer({
  574. key: 'source',
  575. reducer: reducerSource
  576. })
  577. const withSagaSource = injectSaga({ key: 'source', saga: sagasSource })
  578. const withReducerProject = injectReducer({
  579. key: 'project',
  580. reducer: reducerProject
  581. })
  582. const withSagaProject = injectSaga({ key: 'project', saga: sagasProject })
  583. export default compose(
  584. withReducer,
  585. withReducerSource,
  586. withSaga,
  587. withSagaSource,
  588. withReducerProject,
  589. withSagaProject,
  590. withConnect
  591. )(ViewEditor)