Editor.tsx 18 KB

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