/* * << * Davinci * == * Copyright (C) 2016 - 2017 EDP * == * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * >> */ import React from 'react' import { compose, Dispatch } from 'redux' import { connect } from 'react-redux' import { createStructuredSelector } from 'reselect' import memoizeOne from 'memoize-one' import Helmet from 'react-helmet' import injectReducer from 'utils/injectReducer' import injectSaga from 'utils/injectSaga' import reducer from './reducer' import sagas from './sagas' import reducerSource from 'containers/Source/reducer' import sagasSource from 'containers/Source/sagas' import reducerProject from 'containers/Projects/reducer' import sagasProject from 'containers/Projects/sagas' import { RouteComponentWithParams } from 'utils/types' import { hideNavigator } from '../App/actions' import { ViewActions, ViewActionType } from './actions' import { SourceActions, SourceActionType } from 'containers/Source/actions' import { OrganizationActions, OrganizationActionType } from 'containers/Organizations/actions' import { makeSelectEditingView, makeSelectEditingViewInfo, makeSelectSources, makeSelectSchema, makeSelectSqlDataSource, makeSelectSqlLimit, makeSelectSqlValidation, makeSelectLoading, makeSelectChannels, makeSelectTenants, makeSelectBizs, makeSelectIsLastExecuteWholeSql } from './selectors' import { makeSelectProjectRoles } from 'containers/Projects/selectors' import { IView, IViewModel, IViewRoleRaw, IViewRole, IViewVariable, IViewInfo, IExecuteSqlParams, IExecuteSqlResponse, IViewLoading, ISqlValidation, IDacChannel, IDacTenant, IDacBiz } from './types' import { ISource, ISchema } from '../Source/types' import { ViewVariableTypes } from './constants' import { message, notification, Tooltip } from 'antd' import EditorSteps from './components/EditorSteps' import EditorContainer from './components/EditorContainer' import ModelAuth from './components/ModelAuth' import SourceTable from './components/SourceTable' import SqlEditor from './components/SqlEditorByAce' import SqlPreview from './components/SqlPreview' import EditorBottom from './components/EditorBottom' import ViewVariableList from './components/ViewVariableList' import VariableModal from './components/VariableModal' import Styles from './View.less' interface IViewEditorStateProps { editingView: IView editingViewInfo: IViewInfo sources: ISource[] schema: ISchema sqlDataSource: IExecuteSqlResponse sqlLimit: number sqlValidation: ISqlValidation loading: IViewLoading projectRoles: any[] channels: IDacChannel[] tenants: IDacTenant[] bizs: IDacBiz[], isLastExecuteWholeSql: boolean } export enum EExecuteType { whole, single } interface IViewEditorDispatchProps { onHideNavigator: () => void onLoadViewDetail: (viewId: number) => void onLoadSources: (projectId: number) => void onLoadSourceDatabases: (sourceId: number) => void onLoadDatabaseTables: (sourceId: number, databaseName: string) => void onLoadTableColumns: (sourceId: number, databaseName: string, tableName: string) => void onExecuteSql: (params: IExecuteSqlParams, exeType: EExecuteType) => void onAddView: (view: IView, resolve: () => void) => void onEditView: (view: IView, resolve: () => void) => void onUpdateEditingView: (view: IView) => void onUpdateEditingViewInfo: (viewInfo: IViewInfo) => void onSetSqlLimit: (limit: number) => void onLoadDacChannels: () => void, onLoadDacTenants: (channelName: string) => void, onLoadDacBizs: (channelName: string, tenantId: number) => void, onResetState: () => void onLoadProjectRoles: (projectId: number) => void onSetIsLastExecuteWholeSql: (isLastExecuteWholeSql: boolean) => void } type IViewEditorProps = IViewEditorStateProps & IViewEditorDispatchProps & RouteComponentWithParams interface IViewEditorStates { containerHeight: number sqlValidationCode: number init: boolean currentStep: number lastSuccessExecutedSql: string sqlFragment: string } export class ViewEditor extends React.Component { public state: Readonly = { containerHeight: 0, currentStep: 0, sqlValidationCode: null, init: true, lastSuccessExecutedSql: null, sqlFragment: '' } public constructor(props: IViewEditorProps) { super(props) const { onHideNavigator, onLoadSources, onLoadViewDetail, onLoadProjectRoles, onLoadDacChannels, match } = this.props onHideNavigator() const { viewId, projectId } = match.params if (projectId) { onLoadSources(+projectId) onLoadProjectRoles(+projectId) } if (viewId) { onLoadViewDetail(+viewId) } onLoadDacChannels() } public static getDerivedStateFromProps: React.GetDerivedStateFromProps = (props, state) => { const { match, editingView, sqlValidation } = props const { viewId } = match.params const { init, sqlValidationCode } = state let { lastSuccessExecutedSql } = state if (sqlValidationCode !== sqlValidation.code && sqlValidation.code) { notification.destroy() sqlValidation.code === 200 ? notification.success({ message: '执行成功', duration: 3 }) : notification.error({ message: '执行失败', description: ( 点击查看错误信息 ), duration: null }) if (sqlValidation.code === 200) { lastSuccessExecutedSql = editingView.sql } } if (editingView && editingView.id === +viewId) { if (init) { props.onLoadSourceDatabases(editingView.sourceId) lastSuccessExecutedSql = editingView.sql return { init: false, sqlValidationCode: sqlValidation.code, lastSuccessExecutedSql } } } return { sqlValidationCode: sqlValidation.code, lastSuccessExecutedSql } } public componentWillUnmount() { this.props.onResetState() notification.destroy() } private executeSql = () => { const { sqlFragment } = this.state const { onSetIsLastExecuteWholeSql } = this.props if (sqlFragment != null) { onSetIsLastExecuteWholeSql(false) } ViewEditor.ExecuteSql(this.props, this.state.sqlFragment) } private static ExecuteSql = (props: IViewEditorProps, sqlFragment?: string) => { const { onExecuteSql, editingView, editingViewInfo, sqlLimit } = props const { sourceId, sql } = editingView const { variable } = editingViewInfo const updatedParams: IExecuteSqlParams = { sourceId, sql: sqlFragment ?? sql, limit: sqlLimit, variables: variable } const exeType = sqlFragment == null ? EExecuteType.whole : EExecuteType.single onExecuteSql(updatedParams, exeType) } private stepChange = (step: number) => { const { currentStep } = this.state if (currentStep + step < 0) { this.goToViewList() return } const { editingView } = this.props const { name, sourceId, sql } = editingView const errorMessages = ['名称不能为空', '请选择数据源', 'sql 不能为空'] const fieldsValue = [name, sourceId, sql] const hasError = fieldsValue.some((val, idx) => { if (!val) { message.error(errorMessages[idx]) return true } }) if (hasError) { return } this.setState({ currentStep: currentStep + step }, () => { if (this.state.currentStep > 1) { this.saveView() } }) } private saveView = () => { const { onAddView, onEditView, editingView, editingViewInfo, projectRoles, match } = this.props const { projectId } = match.params const { model, variable, roles } = editingViewInfo const { id: viewId } = editingView const validRoles = roles.filter(({ roleId }) => projectRoles && projectRoles.findIndex(({ id }) => id === roleId) >= 0) const updatedView: IView = { ...editingView, projectId: +projectId, model: JSON.stringify(model), variable: JSON.stringify(variable), roles: validRoles.map(({ roleId, columnAuth, rowAuth }) => { const validColumnAuth = columnAuth.filter((c) => !!model[c]) const validRowAuth = rowAuth.filter((r) => { const v = variable.find((v) => v.name === r.name) if (!v) { return false } return (v.type === ViewVariableTypes.Authorization && !v.fromService) }) return { roleId, columnAuth: JSON.stringify(validColumnAuth), rowAuth: JSON.stringify(validRowAuth) } }) } viewId ? onEditView(updatedView, this.goToViewList) : onAddView(updatedView, this.goToViewList) } private goToViewList = () => { const { history, match } = this.props const { projectId } = match.params const prefix = window.localStorage.getItem('inDataService') ?? '' const prefixPath = prefix ? '/' + prefix : prefix history.push(`/project/${projectId}${prefixPath}/views`) } private viewChange = (propName: keyof IView, value: string | number) => { const { editingView, onUpdateEditingView } = this.props const updatedView = { ...editingView, [propName]: value } onUpdateEditingView(updatedView) } private sqlChange = (sql: string) => { this.viewChange('sql', sql) } private sqlSelect = (sqlFragment: string) => { this.setState({ sqlFragment }) } private modelChange = (partialModel: IViewModel) => { const { editingViewInfo, onUpdateEditingViewInfo } = this.props const { model } = editingViewInfo const updatedViewInfo: IViewInfo = { ...editingViewInfo, model: { ...model, ...partialModel } } onUpdateEditingViewInfo(updatedViewInfo) } private variableChange = (updatedVariable: IViewVariable[]) => { const { editingViewInfo, onUpdateEditingViewInfo } = this.props const updatedViewInfo: IViewInfo = { ...editingViewInfo, variable: updatedVariable } onUpdateEditingViewInfo(updatedViewInfo) } /** * 数组长度1为单选,大于1为全选 * @param {IViewRole[]} viewRoles * @private * @memberof ViewEditor */ private viewRoleChange = (viewRoles: IViewRole[]) => { const { editingViewInfo, onUpdateEditingViewInfo } = this.props let updatedRoles: IViewRole[] = [] if (viewRoles.length === 1) { const [viewRole] = viewRoles const { roles } = editingViewInfo updatedRoles = roles.filter((role) => role.roleId !== viewRole.roleId) updatedRoles.push(viewRole) } else { updatedRoles = viewRoles } const updatedViewInfo = { ...editingViewInfo, roles: updatedRoles } onUpdateEditingViewInfo(updatedViewInfo) } private getSqlHints = memoizeOne((sourceId: number, schema: ISchema, variables: IViewVariable[]) => { if (!sourceId) { return {} } const variableHints = variables.reduce((acc, v) => { acc[`$${v.name}$`] = [] return acc }, {}) const { mapDatabases, mapTables, mapColumns } = schema if (!mapDatabases[sourceId]) { return {} } const tableHints: { [tableName: string]: string[] } = Object.values(mapTables).reduce((acc, tablesInfo) => { if (tablesInfo.sourceId !== sourceId) { return acc } tablesInfo.tables.forEach(({ name: tableName }) => { acc[tableName] = [] }) return acc }, {}) Object.values(mapColumns).forEach((columnsInfo) => { if (columnsInfo.sourceId !== sourceId) { return } const { tableName, columns } = columnsInfo if (tableHints[tableName]) { tableHints[tableName] = tableHints[tableName].concat(columns.map((col) => col.name)) } }) const hints = { ...variableHints, ...tableHints } return hints }) public render() { const { sources, schema, sqlDataSource, sqlLimit, loading, projectRoles, channels, tenants, bizs, editingView, editingViewInfo, isLastExecuteWholeSql, onLoadSourceDatabases, onLoadDatabaseTables, onLoadTableColumns, onSetSqlLimit, onLoadDacTenants, onLoadDacBizs } = this.props const { currentStep, lastSuccessExecutedSql, sqlFragment } = this.state const { model, variable, roles: viewRoles } = editingViewInfo const sqlHints = this.getSqlHints(editingView.sourceId, schema, variable) const containerVisible = !currentStep const modelAuthVisible = !!currentStep const nextDisabled = (editingView.sql !== lastSuccessExecutedSql) return ( <>
) } } const mapDispatchToProps = (dispatch) => ({ onHideNavigator: () => dispatch(hideNavigator()), onLoadViewDetail: (viewId: number) => dispatch(ViewActions.loadViewsDetail([viewId], null, true)), onLoadSources: (projectId) => dispatch(SourceActions.loadSources(projectId)), onLoadSourceDatabases: (sourceId) => dispatch(SourceActions.loadSourceDatabases(sourceId)), onLoadDatabaseTables: (sourceId, databaseName) => dispatch(SourceActions.loadDatabaseTables(sourceId, databaseName)), onLoadTableColumns: (sourceId, databaseName, tableName) => dispatch(SourceActions.loadTableColumns(sourceId, databaseName, tableName)), onExecuteSql: (params, exeType?) => dispatch(ViewActions.executeSql(params, exeType)), onSetIsLastExecuteWholeSql: (isLastExecuteWholeSql: boolean) => dispatch(ViewActions.setIsLastExecuteWholeSql(isLastExecuteWholeSql)), onAddView: (view, resolve) => dispatch(ViewActions.addView(view, resolve)), onEditView: (view, resolve) => dispatch(ViewActions.editView(view, resolve)), onUpdateEditingView: (view) => dispatch(ViewActions.updateEditingView(view)), onUpdateEditingViewInfo: (viewInfo: IViewInfo) => dispatch(ViewActions.updateEditingViewInfo(viewInfo)), onSetSqlLimit: (limit: number) => dispatch(ViewActions.setSqlLimit(limit)), onLoadDacChannels: () => dispatch(ViewActions.loadDacChannels()), onLoadDacTenants: (channelName) => dispatch(ViewActions.loadDacTenants(channelName)), onLoadDacBizs: (channelName, tenantId) => dispatch(ViewActions.loadDacBizs(channelName, tenantId)), onResetState: () => dispatch(ViewActions.resetViewState()), onLoadProjectRoles: (projectId) => dispatch(OrganizationActions.loadProjectRoles(projectId)) }) const mapStateToProps = createStructuredSelector({ editingView: makeSelectEditingView(), editingViewInfo: makeSelectEditingViewInfo(), sources: makeSelectSources(), schema: makeSelectSchema(), sqlDataSource: makeSelectSqlDataSource(), sqlLimit: makeSelectSqlLimit(), sqlValidation: makeSelectSqlValidation(), loading: makeSelectLoading(), projectRoles: makeSelectProjectRoles(), channels: makeSelectChannels(), tenants: makeSelectTenants(), bizs: makeSelectBizs(), isLastExecuteWholeSql: makeSelectIsLastExecuteWholeSql() }) const withConnect = connect(mapStateToProps, mapDispatchToProps) const withReducer = injectReducer({ key: 'view', reducer }) const withSaga = injectSaga({ key: 'view', saga: sagas }) const withReducerSource = injectReducer({ key: 'source', reducer: reducerSource }) const withSagaSource = injectSaga({ key: 'source', saga: sagasSource }) const withReducerProject = injectReducer({ key: 'project', reducer: reducerProject }) const withSagaProject = injectSaga({ key: 'project', saga: sagasProject }) export default compose( withReducer, withReducerSource, withSaga, withSagaSource, withReducerProject, withSagaProject, withConnect )(ViewEditor)