Editor.tsx 18 KB

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