Procházet zdrojové kódy

feat: new tree for view list

hi-cactus! před 3 roky
rodič
revize
63d326edfb
31 změnil soubory, kde provedl 5699 přidání a 21 odebrání
  1. 2 1
      app/containers/App/index.tsx
  2. 1 0
      app/containers/App/sagas.ts
  3. 1 1
      app/containers/DataManager/index.tsx
  4. 552 0
      app/containers/DataManagerView/Editor.tsx
  5. 11 0
      app/containers/DataManagerView/Loadable.tsx
  6. 262 0
      app/containers/DataManagerView/View.less
  7. 483 0
      app/containers/DataManagerView/actions.ts
  8. 144 0
      app/containers/DataManagerView/components/CatalogueModal.tsx
  9. 120 0
      app/containers/DataManagerView/components/CopyModal.tsx
  10. 92 0
      app/containers/DataManagerView/components/EditorBottom.tsx
  11. 231 0
      app/containers/DataManagerView/components/EditorContainer.tsx
  12. 20 0
      app/containers/DataManagerView/components/EditorSteps.tsx
  13. 442 0
      app/containers/DataManagerView/components/ModelAuth.tsx
  14. 120 0
      app/containers/DataManagerView/components/ModelAuthModal.tsx
  15. 301 0
      app/containers/DataManagerView/components/SourceTable.tsx
  16. 110 0
      app/containers/DataManagerView/components/SqlEditor.tsx
  17. 260 0
      app/containers/DataManagerView/components/SqlEditorByAce.tsx
  18. 127 0
      app/containers/DataManagerView/components/SqlPreview.tsx
  19. 328 0
      app/containers/DataManagerView/components/VariableModal.tsx
  20. 77 0
      app/containers/DataManagerView/components/ViewVariableList.tsx
  21. 166 0
      app/containers/DataManagerView/constants.ts
  22. 70 0
      app/containers/DataManagerView/index.less
  23. 594 0
      app/containers/DataManagerView/index.tsx
  24. 329 0
      app/containers/DataManagerView/reducer.ts
  25. 377 0
      app/containers/DataManagerView/sagas.ts
  26. 112 0
      app/containers/DataManagerView/selectors.ts
  27. 219 0
      app/containers/DataManagerView/types.ts
  28. 116 0
      app/containers/DataManagerView/util.ts
  29. 1 1
      app/containers/Source/components/SourceConfigModal.tsx
  30. 18 17
      app/containers/View/index.tsx
  31. 13 1
      app/utils/api.ts

+ 2 - 1
app/containers/App/index.tsx

@@ -112,10 +112,11 @@ export class App extends React.PureComponent<AppProps> {
       if (ticket) {
         request(api.getUserInfo + `?ticket=${ticket}`, { method: 'get' })
           .then((data) => {
-            if (data?.code === 200 && data.data) {
+            if (data?.code === 200 && data?.data) {
               setToken(data.data?.token)
               const loginUser = data.data.userInfo
               this.props.onLogged(JSON.parse(loginUser))
+              localStorage.setItem('loginUser', loginUser)
               statistic.sendPrevDurationRecord()
             }
           })

+ 1 - 0
app/containers/App/sagas.ts

@@ -111,6 +111,7 @@ export function* getServerConfigurations(action) {
     setTokenExpired(configurations.jwtToken.timeout)
     yield put(serverConfigurationsGetted(configurations))
   } catch (err) {
+    console.log(err)
     yield put(getServerConfigurationsFail(err))
     errorHandler(err)
   }

+ 1 - 1
app/containers/DataManager/index.tsx

@@ -2,7 +2,7 @@ import { Route, Switch } from 'react-router-dom'
 import { DataManagerWidget as Widget } from 'containers/Widget/Loadable'
 import { Sidebar } from './Loadable'
 import AuthorizedRoute from 'containers/Main/AuthorizedRoute'
-import { View } from 'containers/View/Loadable'
+import { View } from 'containers/DataManagerView/Loadable'
 import { Source } from 'containers/Source/Loadable'
 import { Project } from 'containers/Projects/Loadable'
 import React, { useEffect } from 'react'

+ 552 - 0
app/containers/DataManagerView/Editor.tsx

@@ -0,0 +1,552 @@
+/*
+ * <<
+ * 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<IViewEditorProps, IViewEditorStates> {
+
+  public state: Readonly<IViewEditorStates> = {
+    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<IViewEditorProps, IViewEditorStates>
+    = (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: (
+            <Tooltip
+              placement='bottom'
+              trigger='click'
+              title={sqlValidation.message}
+              overlayClassName={Styles.errorMessage}
+            >
+              <a>点击查看错误信息</a>
+            </Tooltip>
+          ),
+          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<IViewRoleRaw>(({ 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 (
+      <>
+        <Helmet title='数据资产' />
+        <div className={Styles.viewEditor}>
+          <div className={Styles.header}>
+            <div className={Styles.steps}>
+              <EditorSteps current={currentStep} />
+            </div>
+          </div>
+          <EditorContainer
+            visible={containerVisible}
+            variable={variable}
+            onVariableChange={this.variableChange}
+          >
+            <SourceTable
+              key='SourceTable'
+              view={editingView}
+              sources={sources}
+              schema={schema}
+              onViewChange={this.viewChange}
+              onSourceSelect={onLoadSourceDatabases}
+              onDatabaseSelect={onLoadDatabaseTables}
+              onTableSelect={onLoadTableColumns}
+            />
+            <SqlEditor key='SqlEditor' value={editingView.sql} hints={sqlHints} onSqlChange={this.sqlChange}
+                       onSelect={this.sqlSelect} onCmdEnter={this.executeSql} />
+            <SqlPreview key='SqlPreview' size='small' loading={loading.execute} response={sqlDataSource} />
+            <EditorBottom
+              key='EditorBottom'
+              sqlLimit={sqlLimit}
+              loading={loading.execute}
+              nextDisabled={nextDisabled}
+              sqlFragment={sqlFragment}
+              isLastExecuteWholeSql={isLastExecuteWholeSql}
+              onSetSqlLimit={onSetSqlLimit}
+              onExecuteSql={this.executeSql}
+              onStepChange={this.stepChange}
+            />
+            <ViewVariableList key='ViewVariableList' variables={variable} />
+            <VariableModal
+              key='VariableModal'
+              channels={channels}
+              tenants={tenants}
+              bizs={bizs}
+              onLoadDacTenants={onLoadDacTenants}
+              onLoadDacBizs={onLoadDacBizs}
+            />
+          </EditorContainer>
+          <ModelAuth
+            visible={modelAuthVisible}
+            model={model}
+            variable={variable}
+            sqlColumns={sqlDataSource.columns}
+            roles={projectRoles}
+            viewRoles={viewRoles}
+            onModelChange={this.modelChange}
+            onViewRoleChange={this.viewRoleChange}
+            onStepChange={this.stepChange}
+          />
+        </div>
+      </>
+    )
+  }
+}
+
+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)

+ 11 - 0
app/containers/DataManagerView/Loadable.tsx

@@ -0,0 +1,11 @@
+import React from 'react'
+import loadable from 'utils/loadable'
+import { Skeleton } from 'antd'
+
+export const View = loadable(() => import('./'), {
+  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+})
+
+export const ViewEditor = loadable(() => import('./Editor'), {
+  fallback: <Skeleton active={true} paragraph={{ rows: 15 }} />
+})

+ 262 - 0
app/containers/DataManagerView/View.less

@@ -0,0 +1,262 @@
+@import "~assets/less/variable";
+
+.viewEditor {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow-y: scroll;
+
+  .header {
+    height: 64px;
+    line-height: 64px;
+    background-color: @light-bg;
+    border-bottom: 1px solid @border-color-base;
+  }
+
+  .steps {
+    width: 33.33%;
+    margin: auto;
+    height: 100%;
+    display: flex;
+    align-items: center;
+  }
+
+  .containerVertical {
+    flex: 1;
+    display: flex;
+    flex-direction: row;
+    min-width: 0;
+    min-height: 0;
+  }
+
+  .containerHorizontal {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    min-width: 0;
+    min-height: 0;
+  }
+
+  .sider {
+    border-right: 1px solid @border-color-base;
+    display: flex;
+    flex-direction: column;
+
+    & > div {
+      flex: 1;
+      display: flex;
+    }
+
+    :global(.react-resizable-handle) {
+      position: absolute;
+      width: 16px;
+      height: 100%;
+      padding: 0;
+      bottom: 0;
+      right: -8px;
+      cursor: col-resize;
+      background: none;
+    }
+  }
+
+  .right {
+    border-bottom: 1px solid @border-color-base;
+    margin-bottom: -1px;
+    box-sizing: content-box;
+    user-select: none;
+    flex-shrink: 0;
+    display: flex;
+    flex-direction: column;
+
+    :global(.react-resizable-handle) {
+      position: absolute;
+      height: 16px;
+      width: 100%;
+      padding: 0;
+      bottom: 0;
+      cursor: row-resize;
+      background: none;
+    }
+  }
+
+  .sqlEditor {
+    flex: 1;
+    padding: 16px;
+    border-right: 1px solid @border-color-base;
+    min-height: 0;
+    min-width: 0;
+  }
+
+  .preview {
+    padding: 0 16px;
+    overflow: hidden;
+  }
+
+  .authTab {
+    position: relative;
+    margin-left: 16px;
+    .containerHorizontal
+  }
+
+  .authTable {
+    position: absolute;
+    overflow-y: auto;
+    top: 52px;
+    left: 0;
+    right: 0;
+    bottom: 8px;
+    padding-right: 16px;
+    background: #f7f7f7;
+  }
+
+  .bottom {
+    border: 1px solid @border-color-base;
+    border-left: none;
+    padding: 16px;
+
+    .previewInput span {
+      display: inline-block;
+      padding: 0 8px;
+    }
+
+    :global(.ant-input) {
+      width: 72px;
+    }
+
+    .toolBtns {
+      text-align: right;
+
+      button {
+        margin-left: 16px;
+      }
+    }
+  }
+}
+
+.sourceTable {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+
+  :global(.ant-row) {
+    padding: 16px 16px 0 16px;
+  }
+
+  div[class*='col-'] {
+    margin-bottom: 16px;
+  }
+
+  .tree {
+    flex: 1;
+    padding: 0 16px;
+    margin-bottom: 16px;
+    position: relative;
+
+    > ul {
+      overflow: auto;
+      user-select: none;
+      position: absolute;
+      top: 0;
+      bottom: 0;
+      left: 16px;
+      right: 16px;
+    }
+  }
+}
+
+.viewVariable {
+  width: 280px;
+  display: flex;
+  flex-direction: column;
+
+  .viewVariableHeader {
+    display: flex;
+    flex-direction: row;
+    padding: 0 16px;
+    line-height: 1;
+
+    h4 {
+      flex: 1;
+    }
+  }
+
+  :global {
+    .ant-spin-nested-loading {
+      flex: 1;
+    }
+
+    .ant-list-item-action {
+      margin-left: 8px;
+      display: none;
+
+      li {
+        padding: 0 4px;
+      }
+    }
+
+    .ant-list-item:hover .ant-list-item-action {
+      display: block;
+    }
+
+    .ant-list-item-action {
+      margin-left: 0;
+      margin-right: 12px;
+    }
+
+    .ant-tag {
+      width: 55px;
+      text-align: center;
+      margin-left: 16px;
+    }
+  }
+
+  .variableItem {
+    flex: 1;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    padding-right: 16px;
+  }
+}
+
+.sqlPreview {
+  background: #f7f7f7;
+  padding-top: 1px;
+
+  :global {
+    .ant-table-tbody > tr > td {
+      word-break: break-all;
+    }
+  }
+}
+
+.modelAuth {
+  .cellIcon {
+    margin-left: 8px;
+  }
+  .tableControl {
+    width: 160px;
+  }
+  .cellVarValue {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+  }
+  .cellVarCheckbox {
+    margin-right: 8px;
+    margin-bottom: 4px;
+  }
+  .cellVarInput {
+    flex: 1;
+  }
+}
+
+.errorMessage {
+  max-width: 640px;
+  max-height: 480px;
+  overflow-y: auto;
+}
+
+.shortcuts {
+  width: 300px;
+}

+ 483 - 0
app/containers/DataManagerView/actions.ts

@@ -0,0 +1,483 @@
+/*
+ * <<
+ * 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 axios from 'axios'
+import { ActionTypes } from './constants'
+import { returnType } from 'utils/redux'
+import { IDavinciResponse } from 'utils/request'
+import {
+  IViewBase,
+  IView,
+  IExecuteSqlParams,
+  IExecuteSqlResponse,
+  IViewInfo,
+  IDacChannel,
+  IDacTenant,
+  IDacBiz,
+  IViewQueryResponse
+} from './types'
+import { IDataRequestBody } from '../Dashboard/types'
+import { RenderType } from 'containers/Widget/components/Widget'
+import { IDistinctValueReqeustParams } from 'app/components/Control/types'
+import { EExecuteType } from './Editor'
+const CancelToken = axios.CancelToken
+
+export const ViewActions = {
+  viewsLoaded (views: IViewBase[]) {
+    return {
+      type: ActionTypes.LOAD_VIEWS_SUCCESS,
+      payload: {
+        views
+      }
+    }
+  },
+  loadViews (projectId: number, parentId?: number, resolve?: (views: IViewBase[]) => void) {
+    return {
+      type: ActionTypes.LOAD_VIEWS,
+      payload: {
+        projectId,
+        parentId,
+        resolve
+      }
+    }
+  },
+  loadViewsFail () {
+    return {
+      type: ActionTypes.LOAD_VIEWS_FAILURE,
+      payload: {}
+    }
+  },
+
+  viewsDetailLoaded (views: IView[], isEditing: boolean) {
+    return {
+      type: ActionTypes.LOAD_VIEWS_DETAIL_SUCCESS,
+      payload: {
+        views,
+        isEditing
+      }
+    }
+  },
+  loadViewsDetail (
+    viewIds: number[],
+    resolve?: (views: IView[]) => void,
+    isEditing: boolean = false
+  ) {
+    return {
+      type: ActionTypes.LOAD_VIEWS_DETAIL,
+      payload: {
+        viewIds,
+        isEditing,
+        resolve
+      }
+    }
+  },
+  loadViewsDetailFail () {
+    return {
+      type: ActionTypes.LOAD_VIEWS_DETAIL_FAILURE,
+      payload: {}
+    }
+  },
+
+  addView (view: IView, resolve: () => void) {
+    return {
+      type: ActionTypes.ADD_VIEW,
+      payload: {
+        view,
+        resolve
+      }
+    }
+  },
+  viewAdded (result: IView) {
+    return {
+      type: ActionTypes.ADD_VIEW_SUCCESS,
+      payload: {
+        result
+      }
+    }
+  },
+  addViewFail () {
+    return {
+      type: ActionTypes.ADD_VIEW_FAILURE,
+      payload: {}
+    }
+  },
+
+  editView (view: IView, resolve: () => void) {
+    return {
+      type: ActionTypes.EDIT_VIEW,
+      payload: {
+        view,
+        resolve
+      }
+    }
+  },
+  viewEdited (result: IView) {
+    return {
+      type: ActionTypes.EDIT_VIEW_SUCCESS,
+      payload: {
+        result
+      }
+    }
+  },
+  editViewFail () {
+    return {
+      type: ActionTypes.EDIT_VIEW_FAILURE,
+      payload: {}
+    }
+  },
+
+  deleteView (id: number, resolve: (id: number) => void) {
+    return {
+      type: ActionTypes.DELETE_VIEW,
+      payload: {
+        id,
+        resolve
+      }
+    }
+  },
+  viewDeleted (id: number) {
+    return {
+      type: ActionTypes.DELETE_VIEW_SUCCESS,
+      payload: {
+        id
+      }
+    }
+  },
+  deleteViewFail () {
+    return {
+      type: ActionTypes.DELETE_VIEW_FAILURE,
+      payload: {}
+    }
+  },
+
+  copyView (view: IViewBase, resolve: () => void) {
+    return {
+      type: ActionTypes.COPY_VIEW,
+      payload: {
+        view,
+        resolve
+      }
+    }
+  },
+  viewCopied (fromViewId: number, result: IView) {
+    return {
+      type: ActionTypes.COPY_VIEW_SUCCESS,
+      payload: {
+        fromViewId,
+        result
+      }
+    }
+  },
+  copyViewFail () {
+    return {
+      type: ActionTypes.COPY_VIEW_FAILURE,
+      payload: {}
+    }
+  },
+
+  setIsLastExecuteWholeSql (isLastExecuteWholeSql: boolean) {
+    return {
+      type: ActionTypes.IS_LAST_EXECUTE_WHOLE_SQL,
+      payload: {
+        isLastExecuteWholeSql
+      }
+    }
+  },
+
+  executeSql (params: IExecuteSqlParams, exeType: EExecuteType) {
+    return {
+      type: ActionTypes.EXECUTE_SQL,
+      payload: {
+        params,
+        exeType
+      }
+    }
+  },
+  sqlExecuted (result: IDavinciResponse<IExecuteSqlResponse>) {
+    return {
+      type: ActionTypes.EXECUTE_SQL_SUCCESS,
+      payload: {
+        result
+      }
+    }
+  },
+  executeSqlFail (err: IDavinciResponse<any>['header']) {
+    return {
+      type: ActionTypes.EXECUTE_SQL_FAILURE,
+      payload: {
+        err
+      }
+    }
+  },
+  executeSqlCancel () {
+    return {
+      type: ActionTypes.EXECUTE_SQL_CANCEL,
+      payload: {}
+    }
+  },
+
+  updateEditingView (view: IView) {
+    return {
+      type: ActionTypes.UPDATE_EDITING_VIEW,
+      payload: {
+        view
+      }
+    }
+  },
+  updateEditingViewInfo (viewInfo: IViewInfo) {
+    return {
+      type: ActionTypes.UPDATE_EDITING_VIEW_INFO,
+      payload: {
+        viewInfo
+      }
+    }
+  },
+
+  setSqlLimit (limit: number) {
+    return {
+      type: ActionTypes.SET_SQL_LIMIT,
+      payload: {
+        limit
+      }
+    }
+  },
+
+  resetViewState () {
+    return {
+      type: ActionTypes.RESET_VIEW_STATE,
+      payload: {}
+    }
+  },
+
+  /** Actions for fetch external authorization variables values */
+  loadDacChannels () {
+    return {
+      type: ActionTypes.LOAD_DAC_CHANNELS,
+      payload: {}
+    }
+  },
+  dacChannelsLoaded (channels: IDacChannel[]) {
+    return {
+      type: ActionTypes.LOAD_DAC_CHANNELS_SUCCESS,
+      payload: {
+        channels
+      }
+    }
+  },
+  loadDacChannelsFail () {
+    return {
+      type: ActionTypes.LOAD_DAC_CHANNELS_FAILURE,
+      payload: {}
+    }
+  },
+
+  loadDacTenants (channelName: string) {
+    return {
+      type: ActionTypes.LOAD_DAC_TENANTS,
+      payload: {
+        channelName
+      }
+    }
+  },
+  dacTenantsLoaded (tenants: IDacTenant[]) {
+    return {
+      type: ActionTypes.LOAD_DAC_TENANTS_SUCCESS,
+      payload: {
+        tenants
+      }
+    }
+  },
+  loadDacTenantsFail () {
+    return {
+      type: ActionTypes.LOAD_DAC_TENANTS_FAILURE,
+      payload: {}
+    }
+  },
+
+  loadDacBizs (channelName: string, tenantId: number) {
+    return {
+      type: ActionTypes.LOAD_DAC_BIZS,
+      payload: {
+        channelName,
+        tenantId
+      }
+    }
+  },
+  dacBizsLoaded (bizs: IDacBiz[]) {
+    return {
+      type: ActionTypes.LOAD_DAC_BIZS_SUCCESS,
+      payload: {
+        bizs
+      }
+    }
+  },
+  loadDacBizsFail () {
+    return {
+      type: ActionTypes.LOAD_DAC_BIZS_FAILURE,
+      payload: {}
+    }
+  },
+  /** */
+
+  /** Actions for external usages */
+  loadSelectOptions (
+    controlKey: string,
+    requestParams: { [viewId: string]: IDistinctValueReqeustParams },
+    itemId?: number
+  ) {
+    return {
+      type: ActionTypes.LOAD_SELECT_OPTIONS,
+      payload: {
+        controlKey,
+        requestParams,
+        itemId,
+        cancelTokenSource: CancelToken.source()
+      }
+    }
+  },
+  selectOptionsLoaded (
+    controlKey: string,
+    values: object[],
+    itemId?: number
+  ) {
+    return {
+      type: ActionTypes.LOAD_SELECT_OPTIONS_SUCCESS,
+      payload: {
+        controlKey,
+        values,
+        itemId
+      }
+    }
+  },
+  loadSelectOptionsFail (err) {
+    return {
+      type: ActionTypes.LOAD_SELECT_OPTIONS_FAILURE,
+      payload: {
+        err
+      }
+    }
+  },
+
+  loadViewData (
+    id: number,
+    requestParams: IDataRequestBody,
+    resolve: (data: any[]) => void,
+    reject: (error) => void
+  ) {
+    return {
+      type: ActionTypes.LOAD_VIEW_DATA,
+      payload: {
+        id,
+        requestParams,
+        resolve,
+        reject
+      }
+    }
+  },
+  viewDataLoaded () {
+    return {
+      type: ActionTypes.LOAD_VIEW_DATA_SUCCESS
+    }
+  },
+  loadViewDataFail (err) {
+    return {
+      type: ActionTypes.LOAD_VIEW_DATA_FAILURE,
+      payload: {
+        err
+      }
+    }
+  },
+
+  loadColumnDistinctValue(
+    paramsByViewId: {
+      [viewId: string]: Omit<IDistinctValueReqeustParams, 'cache' | 'expired'>
+    },
+    callback: (options?: object[]) => void
+  ) {
+    return {
+      type: ActionTypes.LOAD_COLUMN_DISTINCT_VALUE,
+      payload: {
+        paramsByViewId,
+        callback
+      }
+    }
+  },
+
+  loadViewDataFromVizItem (
+    renderType: RenderType,
+    itemId: number | [number, number],
+    viewId: number,
+    requestParams: any,
+    vizType: 'dashboard' | 'display',
+    statistic
+  ) {
+    return {
+      type: ActionTypes.LOAD_VIEW_DATA_FROM_VIZ_ITEM,
+      payload: {
+        renderType,
+        itemId,
+        viewId,
+        requestParams,
+        vizType,
+        cancelTokenSource: CancelToken.source()
+      },
+      statistic
+    }
+  },
+  viewDataFromVizItemLoaded (
+    renderType: RenderType,
+    itemId: number | [number, number],
+    requestParams: any,
+    result: IViewQueryResponse,
+    vizType: 'dashboard' | 'display',
+    statistic
+  ) {
+    return {
+      type: ActionTypes.LOAD_VIEW_DATA_FROM_VIZ_ITEM_SUCCESS,
+      payload: {
+        renderType,
+        itemId,
+        requestParams,
+        result,
+        vizType
+      },
+      statistic
+    }
+  },
+  loadViewDataFromVizItemFail (
+    itemId: number | [number, number],
+    vizType: 'dashboard' | 'display',
+    errorMessage: string
+  ) {
+    return {
+      type: ActionTypes.LOAD_VIEW_DATA_FROM_VIZ_ITEM_FAILURE,
+      payload: {
+        itemId,
+        vizType,
+        errorMessage
+      }
+    }
+  }
+  /** */
+}
+const mockAction = returnType(ViewActions)
+export type ViewActionType = typeof mockAction
+
+export default ViewActions

+ 144 - 0
app/containers/DataManagerView/components/CatalogueModal.tsx

@@ -0,0 +1,144 @@
+/*
+ * <<
+ * 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 { Modal, Form, Button, Input } from 'antd'
+import { FormComponentProps } from 'antd/lib/form'
+import { ICatalogue, IViewBase } from '../types'
+
+const FormItem = Form.Item
+
+interface ICopyModalProps extends FormComponentProps<ICatalogue> {
+  visible: boolean
+  loading: boolean
+  fromView: ICatalogue
+  onCheckUniqueName: (viewName: string, resolve: () => void, reject: (err: string) => void) => void
+  onSave: (view: ICatalogue) => void
+  onCancel: () => void
+}
+
+export class CatalogueModal extends React.PureComponent<ICopyModalProps> {
+  private formItemStyle = {
+    labelCol: { span: 6 },
+    wrapperCol: { span: 18 }
+  }
+
+  private save = () => {
+    const { form, fromView, onSave } = this.props
+    form.validateFieldsAndScroll((err, fieldsValue) => {
+      if (err) {
+        return
+      }
+      const copyView = { ...fieldsValue, id: fromView?.id }
+      onSave(copyView)
+    })
+  }
+
+  private checkName = (_, value, callback) => {
+    const { onCheckUniqueName } = this.props
+    onCheckUniqueName(value, () => {
+      callback()
+    }, (err) => {
+      callback(err)
+    })
+  }
+
+  private clearFieldsValue = () => {
+    this.props.form.resetFields()
+  }
+
+  public render() {
+    console.log(this.props)
+    const { form, visible, loading, fromView, onCancel } = this.props
+    const { getFieldDecorator } = form
+    // if (!fromView) { return null }
+
+    const modalButtons = [(
+      <Button
+        key="back"
+        size="large"
+        onClick={onCancel}
+      >
+        取 消
+      </Button>
+    ), (
+      <Button
+        disabled={loading}
+        key="submit"
+        size="large"
+        type="primary"
+        onClick={this.save}
+      >
+        保 存
+      </Button>
+    )]
+
+    return (
+      <Modal
+        title={fromView ? '编辑' : '新增'}
+        wrapClassName="ant-modal-small"
+        visible={visible}
+        footer={modalButtons}
+        onCancel={onCancel}
+        afterClose={this.clearFieldsValue}
+      >
+        <Form>
+          <FormItem label="资源名称" {...this.formItemStyle}>
+            {getFieldDecorator<ICatalogue>('name', {
+              validateFirst: true,
+              rules: [
+                { required: true, message: '不能为空' },
+                { validator: this.checkName }
+              ],
+              initialValue: fromView?.name
+            })(<Input autoComplete={'off'} />)}
+          </FormItem>
+          <FormItem label="行业分类" {...this.formItemStyle}>
+            {getFieldDecorator<ICatalogue>('industry', {
+              initialValue: fromView?.industry
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="来源部门" {...this.formItemStyle}>
+            {getFieldDecorator<ICatalogue>('originDept', {
+              initialValue: fromView?.originDept
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="来源系统" {...this.formItemStyle}>
+            {getFieldDecorator<ICatalogue>('originSystem', {
+              initialValue: fromView?.originSystem
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="扩展json" {...this.formItemStyle}>
+            {getFieldDecorator<ICatalogue>('extConfig', {
+              initialValue: fromView?.extConfig
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="描述" {...this.formItemStyle}>
+            {getFieldDecorator<ICatalogue>('description', {
+              initialValue: fromView?.description
+            })(<Input />)}
+          </FormItem>
+        </Form>
+      </Modal>
+    )
+  }
+}
+
+export default Form.create<ICopyModalProps>()(CatalogueModal)

+ 120 - 0
app/containers/DataManagerView/components/CopyModal.tsx

@@ -0,0 +1,120 @@
+/*
+ * <<
+ * 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 { Modal, Form, Button, Input } from 'antd'
+import { FormComponentProps } from 'antd/lib/form'
+import { IViewBase } from '../types'
+const FormItem = Form.Item
+
+interface ICopyModalProps extends FormComponentProps<IViewBase> {
+  visible: boolean
+  loading: boolean
+  fromView: IViewBase
+  onCheckUniqueName: (viewName: string, resolve: () => void, reject: (err: string) => void) => void
+  onCopy: (view: IViewBase) => void
+  onCancel: () => void
+}
+
+export class CopyModal extends React.PureComponent<ICopyModalProps> {
+  private formItemStyle = {
+    labelCol: { span: 6 },
+    wrapperCol: { span: 18 }
+  }
+
+  private save = () => {
+    const { form, fromView, onCopy } = this.props
+    form.validateFieldsAndScroll((err, fieldsValue) => {
+      if (err) { return }
+      const copyView: IViewBase = { ...fieldsValue, id: fromView.id }
+      onCopy(copyView)
+    })
+  }
+
+  private checkName = (_, value, callback) => {
+    const { onCheckUniqueName } = this.props
+    onCheckUniqueName(value, () => {
+      callback()
+    }, (err) => {
+      callback(err)
+    })
+  }
+
+  private clearFieldsValue = () => {
+    this.props.form.resetFields()
+  }
+
+  public render () {
+    const { form, visible, loading, fromView, onCancel } = this.props
+    const { getFieldDecorator } = form
+    if (!fromView) { return null }
+
+    const modalButtons = [(
+      <Button
+        key="back"
+        size="large"
+        onClick={onCancel}
+      >
+        取 消
+      </Button>
+    ), (
+      <Button
+        disabled={loading}
+        key="submit"
+        size="large"
+        type="primary"
+        onClick={this.save}
+      >
+        保 存
+      </Button>
+    )]
+
+    return (
+      <Modal
+        title="复制 View"
+        wrapClassName="ant-modal-small"
+        visible={visible}
+        footer={modalButtons}
+        onCancel={onCancel}
+        afterClose={this.clearFieldsValue}
+      >
+        <Form>
+          <FormItem label="新名称" {...this.formItemStyle}>
+            {getFieldDecorator<IViewBase>('name', {
+              validateFirst: true,
+              rules: [
+                { required: true, message: '不能为空' },
+                { validator: this.checkName }
+              ],
+              initialValue: `${fromView.name}_copy`
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="描述" {...this.formItemStyle}>
+            {getFieldDecorator<IViewBase>('description', {
+              initialValue: fromView.description
+            })(<Input />)}
+          </FormItem>
+        </Form>
+      </Modal>
+    )
+  }
+}
+
+export default Form.create<ICopyModalProps>()(CopyModal)

+ 92 - 0
app/containers/DataManagerView/components/EditorBottom.tsx

@@ -0,0 +1,92 @@
+/*
+ * <<
+ * 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 { Row, Col, Button, InputNumber, Tooltip, Popover, Tag } from 'antd'
+
+import Styles from '../View.less'
+
+export interface IEditorBottomProps {
+  sqlLimit: number
+  loading: boolean
+  nextDisabled: boolean
+  isLastExecuteWholeSql: boolean
+  sqlFragment: string
+  onSetSqlLimit: (limit: number) => void
+  onExecuteSql: () => void
+  onStepChange: (stepChange: number) => void
+}
+
+const stepChange = (onStepChange: IEditorBottomProps['onStepChange'], step: number) => () => {
+  onStepChange(step)
+}
+
+export const EditorBottom = (props: IEditorBottomProps) => {
+  const { sqlLimit, loading, nextDisabled, isLastExecuteWholeSql, sqlFragment, onSetSqlLimit, onExecuteSql, onStepChange } = props
+  const STATUS_BTN_TEXT = !loading ? '执行' : '中止'
+  const SELECT_CONTENT_BTN_TEXT = sqlFragment ? '选中内容' : ''
+  const NEXT_DISABLED_AS_EXECUTE_FRAGMENT_TEXT = !isLastExecuteWholeSql ? '执行完整sql后可用' : ''
+  const NEXT_DISABLED_AS_SQL_CHANGED_TEXT = nextDisabled ? '执行后下一步可用' : NEXT_DISABLED_AS_EXECUTE_FRAGMENT_TEXT
+  const shortcutsContent = (
+    <Row>
+      <Col span={8}>执行 / 中止:</Col>
+      <Col span={16}>
+        <Tag color="orange">Ctrl + Enter</Tag>(Windows)
+      </Col>
+      <Col offset={8} span={16}>
+        <Tag color="orange">Cmd + Enter</Tag>(Mac OS)
+      </Col>
+    </Row>
+  )
+  return (
+    <Row className={Styles.bottom} type="flex" align="middle" justify="start">
+    <Col span={12} className={Styles.previewInput}>
+      <span>展示前</span>
+      <InputNumber value={sqlLimit} onChange={onSetSqlLimit} />
+      <span>条数据</span>
+    </Col>
+    <Col span={12} className={Styles.toolBtns}>
+      <Popover
+        title="快捷键"
+        content={shortcutsContent}
+        overlayClassName={Styles.shortcuts}
+      >
+        <i className="iconfont icon-shortcuts_icon" />
+      </Popover>
+      <Button onClick={stepChange(onStepChange, -1)}>取消</Button>
+      <Button
+        type="primary"
+        icon={!loading ? 'caret-right' : 'pause-circle'}
+        onClick={onExecuteSql}
+      >
+        {STATUS_BTN_TEXT + SELECT_CONTENT_BTN_TEXT}
+      </Button>
+      <Tooltip title={NEXT_DISABLED_AS_SQL_CHANGED_TEXT}>
+        <Button onClick={stepChange(onStepChange, 1)} disabled={nextDisabled || !isLastExecuteWholeSql}>
+          下一步
+        </Button>
+      </Tooltip>
+    </Col>
+  </Row>
+  )
+}
+
+export default React.memo(EditorBottom)

+ 231 - 0
app/containers/DataManagerView/components/EditorContainer.tsx

@@ -0,0 +1,231 @@
+/*
+ * <<
+ * 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 { uuid } from 'utils/util'
+import { IViewVariable } from '../types'
+import { Resizable, ResizeCallbackData } from 'libs/react-resizable'
+
+import { ISqlEditorProps } from './SqlEditorByAce'
+import { IViewVariableListProps } from './ViewVariableList'
+import { IVariableModalProps } from './VariableModal'
+import { ISqlPreviewProps } from './SqlPreview'
+
+import Styles from '../View.less'
+
+type TEditorSubComponents = 'SourceTable' | 'SqlEditor' | 'ViewVariableList' | 'VariableModal' | 'SqlPreview' | 'EditorBottom'
+interface IEditorContainerProps {
+  visible: boolean
+  variable: IViewVariable[]
+  children?: React.ReactNode
+  onVariableChange: (variable: IViewVariable[]) => void
+}
+
+interface IEditorContainerStates {
+  editorHeight: number
+  siderWidth: number
+  previewHeight: number
+  variableModalVisible: boolean
+  editingVariable: IViewVariable
+}
+
+export class EditorContainer extends React.Component<IEditorContainerProps, IEditorContainerStates> {
+
+  private editor = React.createRef<HTMLDivElement>()
+  public static SiderMinWidth = 250
+  public static EditorMinHeight = 100
+  public static DefaultPreviewHeight = 300
+
+  public state: Readonly<IEditorContainerStates> = {
+    editorHeight: 0,
+    siderWidth: EditorContainer.SiderMinWidth,
+    previewHeight: EditorContainer.DefaultPreviewHeight,
+    variableModalVisible: false,
+    editingVariable: null
+  }
+
+  public componentDidMount () {
+    window.addEventListener('resize', this.setEditorHeight, false)
+    // @FIX for this init height, 64px is the height of the hidden navigator in Main.tsx
+    const editorHeight = this.editor.current.clientHeight
+    this.setState({
+      editorHeight
+    })
+  }
+  public componentWillUnmount () {
+    window.removeEventListener('resize', this.setEditorHeight, false)
+  }
+
+  public setEditorHeight = () => {
+    const editorHeight = this.editor.current.clientHeight
+    const { previewHeight, editorHeight: oldEditorHeight } = this.state
+    const newPreviewHeight = Math.min(Math.floor(previewHeight * (editorHeight / oldEditorHeight)), editorHeight)
+    this.setState({
+      editorHeight,
+      previewHeight: newPreviewHeight
+    })
+  }
+
+  private siderResize = (_: any, { size }: ResizeCallbackData) => {
+    const { width } = size
+    this.setState({ siderWidth: width })
+  }
+
+  private previewResize = (_: any, { size }: ResizeCallbackData) => {
+    const { height } = size
+    this.setState(({ editorHeight }) => ({ previewHeight: editorHeight - height }))
+  }
+
+  private addVariable = () => {
+    this.setState({
+      editingVariable: null,
+      variableModalVisible: true
+    })
+  }
+
+  private saveVariable = (updatedVariable: IViewVariable) => {
+    const { variable, onVariableChange } = this.props
+    const updatedViewVariables = [...variable]
+    if (!updatedVariable.key) {
+      updatedVariable.key = uuid(5)
+      updatedViewVariables.push(updatedVariable)
+    } else {
+      const idx = variable.findIndex((v) => v.key === updatedVariable.key)
+      updatedViewVariables[idx] = updatedVariable
+    }
+    onVariableChange(updatedViewVariables)
+    this.setState({
+      variableModalVisible: false
+    })
+  }
+
+  private deleteVariable = (key: string) => {
+    const { variable, onVariableChange } = this.props
+    const updatedViewVariables = variable.filter((v) => v.key !== key)
+    onVariableChange(updatedViewVariables)
+  }
+
+  private editVariable = (variable: IViewVariable) => {
+    this.setState({
+      editingVariable: variable,
+      variableModalVisible: true
+    })
+  }
+
+  private variableNameValidate = (key: string, name: string, callback: (msg?: string) => void) => {
+    const { variable } = this.props
+    const existed = variable.findIndex((v) => ((!key || v.key !== key) && v.name === name)) >= 0
+    if (existed) {
+      callback('名称不能重复')
+      return
+    }
+    callback()
+  }
+
+  private closeVariableModal = () => {
+    this.setState({ variableModalVisible: false })
+  }
+
+  private getChildren = (props: IEditorContainerProps, state: IEditorContainerStates) => {
+    const obj = {}
+    React.Children.forEach(props.children, (child: React.ReactElement<any>) => {
+      const name = child.key as TEditorSubComponents
+      if (name === 'ViewVariableList') {
+        obj[name] = React.cloneElement<IViewVariableListProps>(child, {
+          className: Styles.viewVariable,
+          onAdd: this.addVariable,
+          onDelete: this.deleteVariable,
+          onEdit: this.editVariable
+        })
+      } else if (name === 'VariableModal') {
+        const { variableModalVisible, editingVariable } = this.state
+        obj[name] = React.cloneElement<IVariableModalProps>(child, {
+          visible: variableModalVisible,
+          variable: editingVariable,
+          nameValidator: this.variableNameValidate,
+          onCancel: this.closeVariableModal,
+          onSave: this.saveVariable
+        })
+      } else if (name === 'SqlPreview') {
+        const { previewHeight } = state
+        obj[name] = React.cloneElement<ISqlPreviewProps>(child, { height: previewHeight })
+      } else if (name === 'SqlEditor') {
+        const { previewHeight } = state
+        obj[name] = React.cloneElement<ISqlEditorProps>(child, { sizeChanged: previewHeight })
+      } else {
+        obj[name] = child
+      }
+    })
+    return obj as Record<TEditorSubComponents, React.ReactElement<any>>
+  }
+
+  public render () {
+    const { visible } = this.props
+    const {
+      editorHeight, siderWidth, previewHeight } = this.state
+    const style = visible ? {} : { display: 'none' }
+    const { SourceTable, SqlEditor, SqlPreview, EditorBottom, ViewVariableList, VariableModal } = this.getChildren(this.props, this.state)
+
+    return (
+      <>
+        <div className={Styles.containerVertical} style={style}>
+          <div className={Styles.sider} style={{ width: siderWidth }}>
+            <Resizable
+              axis="x"
+              width={siderWidth}
+              height={0}
+              minConstraints={[EditorContainer.SiderMinWidth, 0]}
+              maxConstraints={[EditorContainer.SiderMinWidth * 2, 0]}
+              onResize={this.siderResize}
+            >
+              <div>{SourceTable}</div>
+            </Resizable>
+          </div>
+          <div className={Styles.containerHorizontal}>
+            <div className={Styles.containerHorizontal} ref={this.editor}>
+              <div className={Styles.right} style={{ height: editorHeight - previewHeight }}>
+                <Resizable
+                  axis="y"
+                  width={0}
+                  height={editorHeight - previewHeight}
+                  minConstraints={[0, EditorContainer.EditorMinHeight]}
+                  maxConstraints={[0, editorHeight]}
+                  onResize={this.previewResize}
+                >
+                  <div className={Styles.containerVertical}>
+                    {SqlEditor}
+                    {ViewVariableList}
+                  </div>
+                </Resizable>
+              </div>
+              <div className={Styles.preview} style={{ height: previewHeight }}>
+                {SqlPreview}
+              </div>
+            </div>
+            {EditorBottom}
+          </div>
+        </div>
+        {VariableModal}
+      </>
+    )
+  }
+}
+
+export default EditorContainer

+ 20 - 0
app/containers/DataManagerView/components/EditorSteps.tsx

@@ -0,0 +1,20 @@
+import React from 'react'
+import { Steps } from 'antd'
+const Step = Steps.Step
+
+interface IEditorStepProps {
+  current: number
+}
+
+export const EditorSteps: React.FunctionComponent<IEditorStepProps> = (props) => {
+  const { current } = props
+
+  return (
+    <Steps current={current}>
+      <Step title="编写 SQL" />
+      <Step title="编辑数据模型与权限" />
+    </Steps>
+  )
+}
+
+export default EditorSteps

+ 442 - 0
app/containers/DataManagerView/components/ModelAuth.tsx

@@ -0,0 +1,442 @@
+import React from 'react'
+import classnames from 'classnames'
+import memoizeOne from 'memoize-one'
+import { Table, Tabs, Radio, Checkbox, Select, Row, Col, Button, Tag, Tooltip, Icon } from 'antd'
+const { Column } = Table
+const { TabPane } = Tabs
+const RadioGroup = Radio.Group
+const { Option } = Select
+import { RadioChangeEvent } from 'antd/lib/radio'
+import { CheckboxChangeEvent } from 'antd/lib/checkbox'
+import { TableProps, ColumnProps } from 'antd/lib/table'
+
+import { IViewVariable, IViewModelProps, IViewModel, IExecuteSqlResponse, IViewRole, IViewRoleRowAuth } from '../types'
+import {
+  ViewModelTypesLocale,
+  ViewVariableValueTypes,
+  ViewModelVisualTypesLocale,
+  ViewVariableTypes
+} from '../constants'
+
+import OperatorTypes from 'utils/operatorTypes'
+import ConditionValuesControl, { ConditionValueTypes } from 'components/ConditionValuesControl'
+import ModelAuthModal from './ModelAuthModal'
+import Styles from '../View.less'
+
+interface IViewRoleRowAuthConverted {
+  name: string
+  values: Array<string | number | boolean>
+  enable: boolean
+  variable: IViewVariable
+}
+
+interface IViewRoleConverted {
+  roleId: number
+  roleName: string
+  roleDesc: string
+  columnAuth: string[]
+  rowAuthConverted: {
+    [variableName: string]: IViewRoleRowAuthConverted
+  }
+}
+
+interface IModelAuthProps {
+  visible: boolean
+  model: IViewModel
+  variable: IViewVariable[]
+  sqlColumns: IExecuteSqlResponse['columns']
+  roles: any[] // @FIXME role typing
+  viewRoles: IViewRole[]
+  onModelChange: (partialModel: IViewModel) => void
+  onViewRoleChange: (viewRole: IViewRole[]) => void
+  onStepChange: (stepChange: number) => void
+}
+
+enum EAllCheckedCheckboxStatus {
+  empty = 1,
+  indeterminate,
+  allChecked
+}
+interface IAllCheckedCheckboxStatus { [variableName: string]: EAllCheckedCheckboxStatus }
+interface IModelAuthStates {
+  modalVisible: boolean
+  selectedRoleId: number
+  selectedColumnAuth: string[]
+  allCheckedCheckboxStatus: IAllCheckedCheckboxStatus
+}
+
+export class ModelAuth extends React.PureComponent<IModelAuthProps, IModelAuthStates> {
+
+  private authDatasourceMap = new Map<number, IViewRoleConverted>()
+
+  public state: Readonly<IModelAuthStates> = {
+    modalVisible: false,
+    selectedRoleId: 0,
+    selectedColumnAuth: [],
+    allCheckedCheckboxStatus: {}
+  }
+
+  public componentDidUpdate () {
+    this.checkDataToChangeAllCheckedCheckboxStatus()
+  }
+
+  private modelTypeOptions = Object.entries(ViewModelTypesLocale).map(([value, label]) => ({
+    label,
+    value
+  }))
+
+  private visualTypeOptions = Object.entries(ViewModelVisualTypesLocale).map(([visualType, text]) => (
+    <Option key={visualType} value={visualType}>{text}</Option>
+  ))
+
+  private modelChange = (record: IViewModelProps, propName: keyof IViewModelProps) => (e: RadioChangeEvent | string) => {
+    const value: string = (e as RadioChangeEvent).target ? (e as RadioChangeEvent).target.value : e
+    const { name, ...rest } = record
+    const partialModel: IViewModel = {
+      [name]: {
+        ...rest,
+        [propName]: value
+      }
+    }
+    this.props.onModelChange(partialModel)
+  }
+
+  private stepChange = (step: number) => () => {
+    this.props.onStepChange(step)
+  }
+
+  private setColumnAuth = (viewRole: IViewRoleConverted) => () => {
+    const { roleId, columnAuth } = viewRole
+    const { model } = this.props
+    this.setState({
+      modalVisible: true,
+      selectedRoleId: roleId,
+      selectedColumnAuth: columnAuth.filter((column) => !!model[column])
+    })
+  }
+
+  private allCheckedCheckboxStatusChange = (roleId: number, rowAuthConverted: IViewRoleRowAuthConverted) => {
+    const { name, enable: checked } = rowAuthConverted
+    this.setState((prevState, props) => {
+      const { allCheckedCheckboxStatus } = prevState
+      let status = allCheckedCheckboxStatus[name]
+      const localItem = this.authDatasourceMap.get(roleId)
+      localItem.rowAuthConverted[name] = rowAuthConverted
+      this.authDatasourceMap.set(roleId, localItem)
+      if (checked) {
+        const isAllChecked = [...this.authDatasourceMap.values()].every((viewRoleConverted) => viewRoleConverted.rowAuthConverted[name]?.enable)
+        status = isAllChecked ? EAllCheckedCheckboxStatus.allChecked : EAllCheckedCheckboxStatus.indeterminate
+      } else {
+        const isEmpty = [...this.authDatasourceMap.values()].every((viewRoleConverted) => !viewRoleConverted.rowAuthConverted[name]?.enable)
+        status = isEmpty ? EAllCheckedCheckboxStatus.empty : EAllCheckedCheckboxStatus.indeterminate
+      }
+      if (status !== allCheckedCheckboxStatus[name]) {
+        return { allCheckedCheckboxStatus: { ...allCheckedCheckboxStatus, [name]: status } }
+      }
+    })
+  }
+
+  private checkDataToChangeAllCheckedCheckboxStatus = () => {
+    this.authDatasourceMap.forEach((viewRoleConverted, roleId) => {
+      const { rowAuthConverted } = viewRoleConverted
+      Object.values(rowAuthConverted).forEach((viewRoleRowAuthConverted) => {
+        this.allCheckedCheckboxStatusChange(roleId, viewRoleRowAuthConverted)
+      })
+    })
+  }
+
+  private rowAuthCheckedChange = (roleId: number, rowAuthConverted: IViewRoleRowAuthConverted) => (e: CheckboxChangeEvent) => {
+    const checked = e.target.checked
+    const { name, values } = rowAuthConverted
+    const updatedRoleAuth: IViewRoleRowAuth = {
+      name,
+      values,
+      enable: checked
+    }
+    this.viewRoleChange(roleId, updatedRoleAuth)
+    this.allCheckedCheckboxStatusChange(roleId, { ...rowAuthConverted, enable: checked })
+  }
+
+  private rowAuthCheckedChangeAll = (variableName: string) => (e: CheckboxChangeEvent) => {
+    const checked = e.target.checked
+    this.viewRoleChangeAll(variableName, checked)
+  }
+
+  private viewRoleChangeAll = (checkedVariableName: string, checked: boolean) => {
+    const { onViewRoleChange } = this.props
+    const viewRoles = [...this.authDatasourceMap].map(([roleId, viewRoleConverted]) => {
+      const { columnAuth, rowAuthConverted } = viewRoleConverted
+      const rowAuth = Object.entries(rowAuthConverted).map(([variableName, viewRoleAuthConverted]) => {
+        const { name, values, enable } = viewRoleAuthConverted
+        return { name, values, enable: checkedVariableName === name ? checked : enable }
+      })
+      return { roleId, columnAuth, rowAuth }
+    })
+    onViewRoleChange(viewRoles)
+  }
+
+  private rowAuthValuesChange = (roleId: number, rowAuthConverted: IViewRoleRowAuthConverted) => (values: Array<string | number | boolean>) => {
+    const { name, enable } = rowAuthConverted
+    const updatedRoleAuth: IViewRoleRowAuth = {
+      name,
+      values,
+      enable
+    }
+    this.viewRoleChange(roleId, updatedRoleAuth)
+  }
+
+  private viewRoleChange = (roleId: number, updatedRoleAuth: IViewRoleRowAuth) => {
+    const { onViewRoleChange, viewRoles } = this.props
+    let viewRole = viewRoles.find((v) => v.roleId === roleId)
+    if (!viewRole) {
+      viewRole = {
+        roleId,
+        columnAuth: [],
+        rowAuth: [updatedRoleAuth]
+      }
+    } else {
+      const variableIdx = viewRole.rowAuth.findIndex((auth) => auth.name === updatedRoleAuth.name)
+      if (variableIdx < 0) {
+        viewRole.rowAuth.push(updatedRoleAuth)
+      } else {
+        viewRole.rowAuth[variableIdx].values = updatedRoleAuth.values
+        viewRole.rowAuth[variableIdx].enable = updatedRoleAuth.enable
+      }
+    }
+    onViewRoleChange([{ ...viewRole }])
+  }
+
+  private getAuthTableColumns = memoizeOne((model: IViewModel, variables: IViewVariable[], allCheckedCheckboxStatus: IAllCheckedCheckboxStatus) => {
+    const columnsChildren = variables
+      .filter((v) => (v.type === ViewVariableTypes.Authorization && !v.fromService))
+      .map<ColumnProps<IViewRoleConverted>>((variable) => ({
+        title: (
+          <>
+            <Checkbox
+              checked={allCheckedCheckboxStatus?.[variable.name] === EAllCheckedCheckboxStatus.allChecked}
+              indeterminate={allCheckedCheckboxStatus?.[variable.name] === EAllCheckedCheckboxStatus.indeterminate}
+              className={Styles.cellVarCheckbox}
+              onChange={this.rowAuthCheckedChangeAll(variable.name)}
+            />
+            {`${variable.alias || variable.name}`}
+          </>
+        ),
+        dataIndex: 'rowAuthConverted' + variable.key,
+        width: 250,
+        render: (_, record: IViewRoleConverted) => {
+          const { name: variableName, valueType } = variable
+          const { roleId, rowAuthConverted } = record
+          const { values: rowAuthValues, enable } = rowAuthConverted[variableName]
+          const operatorType = (valueType === ViewVariableValueTypes.Boolean ? OperatorTypes.Equal : OperatorTypes.In)
+          return (
+            <div className={Styles.cellVarValue}>
+              <Tooltip title={enable ? '禁用' : '启用'}>
+                <Checkbox
+                  checked={enable}
+                  className={Styles.cellVarCheckbox}
+                  onChange={this.rowAuthCheckedChange(roleId, rowAuthConverted[variableName])}
+                />
+              </Tooltip>
+              {enable && (
+                <ConditionValuesControl
+                  className={Styles.cellVarInput}
+                  size="default"
+                  visualType={valueType}
+                  operatorType={operatorType}
+                  conditionValues={rowAuthValues}
+                  onChange={this.rowAuthValuesChange(roleId, rowAuthConverted[variableName])}
+                />
+              )}
+            </div>
+          )
+        }
+      }))
+    const columns: Array<ColumnProps<IViewRoleConverted>> = [{
+      title: '角色',
+      dataIndex: 'roleName',
+      width: 300,
+      render: (roleName: string, record: IViewRoleConverted) => (
+        <span>
+          {roleName}
+          {record.roleDesc && (
+            <Tooltip title={record.roleDesc}>
+              <Icon className={Styles.cellIcon} type="info-circle" />
+            </Tooltip>
+          )}
+        </span>
+      )
+    }]
+    if (columnsChildren.length > 0) {
+      columns.push({
+        title: '权限变量值设置',
+        children: columnsChildren
+      })
+    }
+    columns.push({
+      title: '可见字段',
+      dataIndex: 'columnAuth',
+      width: 120,
+      render: (columnAuth: string[], record) => {
+        if (columnAuth.length === 0) {
+          return (<Tag onClick={this.setColumnAuth(record)}>全部可见</Tag>)
+        }
+        if (columnAuth.length === Object.keys(model).length) {
+          return (<Tag onClick={this.setColumnAuth(record)} color="#f50">不可见</Tag>)
+        }
+        return (<Tag color="green" onClick={this.setColumnAuth(record)}>部分可见</Tag>)
+      }
+    })
+    return columns
+  })
+
+  private getAuthTableScroll = memoizeOne((columns: Array<ColumnProps<any>>) => {
+    const scroll: TableProps<any>['scroll'] = {}
+    const columnsTotalWidth = columns.reduce((acc, c) => acc + (c.width as number), 0)
+    scroll.x = columnsTotalWidth
+    return scroll
+  })
+
+  private getAuthDatasource = (roles: any[], varibles: IViewVariable[], viewRoles: IViewRole[]) => {
+    if (!Array.isArray(roles)) { return [] }
+    const authDatasourceMap = new Map<number, IViewRoleConverted>()
+    const authDatasource = roles.map<IViewRoleConverted>((role) => {
+      const { id: roleId, name: roleName, description: roleDesc } = role
+      const viewRole = viewRoles.find((v) => v.roleId === roleId)
+      const columnAuth = viewRole ? viewRole.columnAuth : []
+      const rowAuthConverted = varibles.reduce<IViewRoleConverted['rowAuthConverted']>((obj, variable) => {
+        const { name: variableName, type, fromService } = variable
+        if (type === ViewVariableTypes.Query) { return obj }
+        if (type === ViewVariableTypes.Authorization && fromService) { return obj }
+
+        const authIdx = viewRole ? viewRole.rowAuth.findIndex((auth) => auth.name === variableName) : -1
+        obj[variableName] = {
+          name: variableName,
+          values: [],
+          enable: false,
+          variable
+        }
+        if (authIdx >= 0) {
+          const { enable, values } = viewRole.rowAuth[authIdx]
+          obj[variableName] = {
+            ...obj[variableName],
+            enable,
+            values
+          }
+        }
+        return obj
+      }, {})
+      const authDatasourceItem = {
+        roleId,
+        roleName,
+        roleDesc,
+        columnAuth,
+        rowAuthConverted
+      }
+      authDatasourceMap.set(roleId, authDatasourceItem)
+      return authDatasourceItem
+    })
+    this.authDatasourceMap = authDatasourceMap
+    return authDatasource
+  }
+
+  private renderColumnModelType = (text: string, record) => (
+    <RadioGroup
+      options={this.modelTypeOptions}
+      value={text}
+      onChange={this.modelChange(record, 'modelType')}
+    />
+  )
+
+  private renderColumnVisualType = (text: string, record) => (
+    <Select
+      className={Styles.tableControl}
+      value={text}
+      onChange={this.modelChange(record, 'visualType')}
+    >
+      {this.visualTypeOptions}
+    </Select>
+  )
+
+  private saveModelAuth = (columnAuth: string[]) => {
+    const { onViewRoleChange, viewRoles } = this.props
+    const { selectedRoleId } = this.state
+    let viewRole = viewRoles.find((v) => v.roleId === selectedRoleId)
+    if (!viewRole) {
+      viewRole = {
+        roleId: selectedRoleId,
+        columnAuth,
+        rowAuth: []
+      }
+    } else {
+      viewRole = {
+        ...viewRole,
+        columnAuth
+      }
+    }
+    onViewRoleChange([viewRole])
+    this.closeModelAuth()
+  }
+
+  private closeModelAuth = () => {
+    this.setState({ modalVisible: false })
+  }
+
+  public render () {
+    const { visible, model, variable, viewRoles, sqlColumns, roles, onModelChange } = this.props
+    const { modalVisible, selectedColumnAuth, selectedRoleId, allCheckedCheckboxStatus } = this.state
+    const modelDatasource = Object.entries(model).map(([name, value]) => ({ name, ...value }))
+    const authColumns = this.getAuthTableColumns(model, variable, allCheckedCheckboxStatus)
+    const authScroll = this.getAuthTableScroll(authColumns)
+    const authDatasource = this.getAuthDatasource(roles, variable, viewRoles)
+    const styleCls = classnames({
+      [Styles.containerHorizontal]: true,
+      [Styles.modelAuth]: true
+    })
+    const style = visible ? {} : { display: 'none' }
+
+    return (
+      <div className={styleCls} style={style}>
+        <Tabs defaultActiveKey="model" className={Styles.authTab}>
+          <TabPane tab="Model" key="model">
+            <div className={Styles.authTable}>
+              <Table bordered pagination={false} rowKey="name" dataSource={modelDatasource}>
+                <Column title="字段名称" dataIndex="name" />
+                <Column title="数据类型" dataIndex="modelType" render={this.renderColumnModelType} />
+                <Column title="可视化类型" dataIndex="visualType" render={this.renderColumnVisualType} />
+              </Table>
+            </div>
+          </TabPane>
+          <TabPane tab="Auth" key="auth">
+            <div className={Styles.authTable}>
+              <Table
+                bordered
+                rowKey="roleId"
+                pagination={false}
+                columns={authColumns}
+                scroll={authScroll}
+                dataSource={authDatasource}
+              />
+            </div>
+            <ModelAuthModal
+              visible={modalVisible}
+              model={model}
+              roleId={selectedRoleId}
+              auth={selectedColumnAuth}
+              onSave={this.saveModelAuth}
+              onCancel={this.closeModelAuth}
+            />
+          </TabPane>
+        </Tabs>
+        <Row className={Styles.bottom} type="flex" align="middle" justify="end">
+          <Col span={12} className={Styles.toolBtns}>
+            <Button type="primary" onClick={this.stepChange(-1)}>上一步</Button>
+            <Button onClick={this.stepChange(-2)}>取消</Button>
+            <Button onClick={this.stepChange(1)}>保存</Button>
+          </Col>
+        </Row>
+      </div>
+    )
+  }
+}
+
+export default ModelAuth

+ 120 - 0
app/containers/DataManagerView/components/ModelAuthModal.tsx

@@ -0,0 +1,120 @@
+import React from 'react'
+import { Modal, Button, List, Checkbox } from 'antd'
+const ListItem = List.Item
+import { CheckboxChangeEvent } from 'antd/lib/checkbox'
+import { IViewModel } from '../types'
+
+interface IModelAuthModalProps {
+  visible: boolean
+  model: IViewModel
+  roleId: number
+  auth: string[]
+  onSave: (auth: string[]) => void
+  onCancel: () => void
+}
+
+interface IModelAuthModalStates {
+  localRoleId: number
+  localAuth: string[]
+}
+
+export class ModelAuthModal extends React.PureComponent<IModelAuthModalProps, IModelAuthModalStates> {
+
+  public state: Readonly<IModelAuthModalStates> = {
+    localRoleId: 0,
+    localAuth: []
+  }
+
+  public static getDerivedStateFromProps:
+    React.GetDerivedStateFromProps<IModelAuthModalProps, IModelAuthModalStates>
+  = (props, state) => {
+    const { roleId, auth } = props
+    const { localRoleId } = state
+    if (roleId !== localRoleId) {
+      return {
+        localRoleId: roleId,
+        localAuth: [...auth] }
+    }
+    return null
+  }
+
+  private save = () => {
+    this.props.onSave([...this.state.localAuth])
+  }
+
+  private modalFooter = [(
+    <Button key="cancel" size="large" onClick={this.props.onCancel}>取 消</Button>
+  ), (
+    <Button key="save" size="large" type="primary" onClick={this.save}>保 存</Button>
+  )]
+
+  private toggleCheckAll = (e: CheckboxChangeEvent) => {
+    const localAuth = e.target.checked ? [] : Object.keys(this.props.model)
+    this.setState({ localAuth })
+  }
+
+  private toggleCheck = (name: string) => (e: CheckboxChangeEvent) => {
+    const checked = e.target.checked
+    this.setState(({ localAuth }) => {
+      if (checked) {
+        return { localAuth: localAuth.filter((item) => item !== name) }
+      }
+      return { localAuth: [...localAuth, name] }
+    })
+  }
+
+  private renderListHeader (props: IModelAuthModalProps) {
+    const { model } = props
+    const { localAuth } = this.state
+    const indeterminate = (localAuth.length > 0 && localAuth.length !== Object.keys(model).length)
+    const checkAll = (localAuth.length === 0)
+
+    return (
+      <Checkbox
+        indeterminate={indeterminate}
+        onChange={this.toggleCheckAll}
+        checked={checkAll}
+      >
+        全选
+      </Checkbox>
+    )
+  }
+
+  private renderItem = (name: string) => {
+    const { localAuth } = this.state
+    const checked = !localAuth.includes(name)
+    return (
+      <ListItem>
+        <Checkbox
+          onChange={this.toggleCheck(name)}
+          checked={checked}
+        >
+          {name}
+        </Checkbox>
+      </ListItem>
+    )
+  }
+
+  public render () {
+    const { visible, model, onCancel } = this.props
+    const listDataSource = Object.keys(model)
+
+    return (
+      <Modal
+        title="勾选可见字段"
+        visible={visible}
+        footer={this.modalFooter}
+        onCancel={onCancel}
+      >
+        <List
+          bordered
+          header={this.renderListHeader(this.props)}
+          dataSource={listDataSource}
+          renderItem={this.renderItem}
+        />
+      </Modal>
+    )
+  }
+}
+
+export default ModelAuthModal

+ 301 - 0
app/containers/DataManagerView/components/SourceTable.tsx

@@ -0,0 +1,301 @@
+import React from 'react'
+import memoizeOne from 'memoize-one'
+
+import { Input, Select, Row, Col, Tree, Icon } from 'antd'
+import { AntTreeNode, AntTreeNodeSelectedEvent, AntTreeNodeExpandedEvent } from 'antd/lib/tree/Tree'
+const { Search } = Input
+const { Option } = Select
+const { TreeNode } = Tree
+
+import { ISource, IColumn, ISchema } from 'containers/Source/types'
+import { IView } from '../types'
+import { SQL_DATE_TYPES, SQL_NUMBER_TYPES, SQL_STRING_TYPES } from 'app/globalConstants'
+import { filterSelectOption } from 'app/utils/util'
+
+import utilStyles from 'assets/less/util.less'
+import Styles from 'containers/View/View.less'
+import { shallowEqual } from 'react-redux'
+
+interface ISourceTableProps {
+  view: IView
+  sources: ISource[]
+  schema: ISchema
+  onViewChange: (propName: keyof(IView), value: string | number) => void
+  onSourceSelect: (sourceId: number) => void
+  onDatabaseSelect: (sourceId: number, databaseName: string) => void
+  onTableSelect: (sourceId: number, databaseName: string, tableName: string) => void
+}
+
+interface ISourceTableStates {
+  filterKeyword: string
+  expandedNodeKeys: string[]
+  autoExpandTable: boolean
+}
+
+export class SourceTable extends React.Component<ISourceTableProps, ISourceTableStates> {
+
+  public state: ISourceTableStates = {
+    filterKeyword: '',
+    expandedNodeKeys: [],
+    autoExpandTable: true
+  }
+
+  private inputChange = (propName: keyof IView) => (e: React.ChangeEvent<HTMLInputElement>) => {
+    this.props.onViewChange(propName, e.target.value)
+  }
+
+  private selectSource = (sourceId: number) => {
+    const { onViewChange, onSourceSelect } = this.props
+    this.setState({
+      expandedNodeKeys: [],
+      autoExpandTable: true
+    })
+    onViewChange('sourceId', sourceId)
+    onSourceSelect(sourceId)
+  }
+
+  private iconDatabase = <Icon key="iconDatabase" title="数据库" type="database" />
+  private iconTable = <Icon key="iconTable" title="数据表" type="table" />
+  private iconDate = <Icon key="iconDate" title="日期" type="calendar" />
+  private iconKey = <Icon key="iconKey" title="主键" type="key" />
+  private iconText = <Icon key="iconText" title="文本" type="font-size" />
+  private iconValue = <Icon key="iconValue" title="数值" type="calculator" />
+  private getColumnIcons (col: IColumn, primaryKeys: string[]) {
+    const { type: sqlType, name } = col
+    if (primaryKeys.includes(name)) { return this.iconKey }
+    if (SQL_STRING_TYPES.includes(sqlType)) { return this.iconText }
+    if (SQL_NUMBER_TYPES.includes(sqlType)) { return this.iconValue }
+    if (SQL_DATE_TYPES.includes(sqlType)) { return this.iconDate }
+  }
+
+  private highlightTitle (title: string, regex: RegExp) {
+    if (!title || !regex) { return title }
+    return (
+      <span
+        dangerouslySetInnerHTML={{
+          __html: title.replace(regex, `<span class="${utilStyles.highlight}">$1</span>`)
+        }}
+      />
+    )
+  }
+
+  private renderTableColumns =  memoizeOne((
+    sourceId: number, schema: ISchema, filterKeyword: string, onDatabaseSelect: ISourceTableProps['onDatabaseSelect']) => {
+      const { mapDatabases, mapTables, mapColumns } = schema
+      if (!sourceId) { return null }
+      const databasesInfo = mapDatabases[sourceId]
+      if (!databasesInfo) { return null }
+
+      const filterReg = filterKeyword ? new RegExp(`(${filterKeyword})`, 'i') : null
+
+      const treeNodes = databasesInfo.reduce((databaseNodes, dbName) => {
+        const tablesInfo = mapTables[`${sourceId}_${dbName}`]
+        if (!tablesInfo) {
+          if (Object.values(databasesInfo).length === 1) {
+            onDatabaseSelect(sourceId, dbName)
+            return databaseNodes
+          }
+          databaseNodes.push(<TreeNode icon={this.iconDatabase} title={dbName} key={dbName} isLeaf={false} dataRef={['database', dbName]} />)
+          return databaseNodes
+        }
+        let filterTables = tablesInfo.tables
+        if (filterReg) {
+          filterTables = filterTables.filter(({ name: tableName }) => {
+            if (filterReg.test(tableName)) { return true }
+
+            const columnsInfo = mapColumns[[sourceId, dbName, tableName].join('_')]
+            if (!columnsInfo) { return false }
+            const hasFilterColumns = columnsInfo.columns.some((col) => filterReg.test(col.name))
+            return hasFilterColumns
+          })
+        }
+
+        const tableNodes = filterTables.map(({ name: tableName }) => {
+          const columnsInfo = mapColumns[[sourceId, dbName, tableName].join('_')]
+
+          const columnNodes = !columnsInfo ? null : columnsInfo.columns.reduce((nodes, col) => {
+            // if (filterReg && !filterReg.test(col.name)) { return nodes }
+
+            const primaryKeysRemain = [...columnsInfo.primaryKeys]
+            const icons = this.getColumnIcons(col, columnsInfo.primaryKeys)
+            const columnTitle = this.highlightTitle(col.name, filterReg)
+            const currentNode = (
+              <TreeNode title={columnTitle} icon={icons} key={`${dbName}_${tableName}_${col.name}`} isLeaf={true} dataRef={['column']} />
+            )
+            if (primaryKeysRemain.includes(col.name)) {
+              // make the primary key column be the top
+              nodes.splice(columnsInfo.primaryKeys.length - primaryKeysRemain.length, 0, currentNode)
+              primaryKeysRemain.splice(primaryKeysRemain.indexOf(col.name), 1)
+            } else {
+              nodes.push(currentNode)
+            }
+            return nodes
+          }, [])
+
+          return (<TreeNode icon={this.iconTable} title={tableName} key={`${dbName}_${tableName}`} isLeaf={false} dataRef={['table', dbName, tableName]}>{columnNodes}</TreeNode>)
+        })
+
+        const nodes = Object.values(databasesInfo).length === 1 ? tableNodes
+          : (<TreeNode icon={this.iconDatabase} title={dbName} key={dbName} isLeaf={false} dataRef={['database']}>{tableNodes}</TreeNode>)
+        databaseNodes.push(nodes)
+        return databaseNodes
+      }, [])
+
+      return treeNodes
+    }
+  )
+
+  private loadTreeData = (node: AntTreeNode) => new Promise<void>((resolve) => {
+    const { dataRef } = node.props
+    if (dataRef === 'column') {
+      resolve()
+      return
+    }
+
+    const { schema, view, onDatabaseSelect, onTableSelect } = this.props
+    const { sourceId } = view
+    const { mapTables, mapColumns } = schema
+
+    const [nodeType, dbName, tableName] = dataRef
+    switch (nodeType) {
+      case 'database':
+        if (!mapTables[`${sourceId}_${dbName}`]) {
+          onDatabaseSelect(sourceId, dbName)
+        }
+        break
+      case 'table':
+        if (!mapColumns[`${sourceId}_${dbName}_${tableName}`]) {
+          onTableSelect(sourceId, dbName, tableName)
+        }
+        break
+    }
+    resolve()
+  })
+
+  private treeNodeSelect = (_: string[], { node }: AntTreeNodeSelectedEvent) => {
+    const { dataRef, eventKey: nodeKey } = node.props
+    const [nodeType] = dataRef
+    if (nodeType === 'column') { return }
+
+    const { expandedNodeKeys } = this.state
+    if (expandedNodeKeys.includes(nodeKey)) { return }
+
+    this.setState({
+      expandedNodeKeys: [...expandedNodeKeys, nodeKey],
+      autoExpandTable: false
+    })
+  }
+
+  private treeNodeExpand = (expandedNodeKeys: string[]) => {
+    this.setState({
+      expandedNodeKeys,
+      autoExpandTable: false
+    })
+  }
+
+  private filterKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const filterKeyword = e.target.value
+    const { schema, view } = this.props
+    const { mapTables, mapColumns } = schema
+    const expandedNodeKeys = new Set<string>()
+    if (filterKeyword) {
+      const regex = new RegExp(`(${filterKeyword})`, 'gi')
+
+      Object.values(mapTables).forEach((tablesInfo) => {
+        if (!tablesInfo) { return }
+        const { tables, dbName, sourceId } = tablesInfo
+        if (sourceId !== view.sourceId) { return }
+        const shouldExpand = regex.test(dbName) ||
+          tables.some(({ name: tableName }) => regex.test(tableName))
+        if (shouldExpand) {
+          expandedNodeKeys.add(dbName)
+        }
+      })
+
+      Object.values(mapColumns).forEach((columnsInfo) => {
+        if (!columnsInfo) { return }
+        const { columns, tableName, dbName, sourceId } = columnsInfo
+        if (sourceId !== view.sourceId) { return }
+        const shouldExpand = regex.test(tableName) ||
+          columns.some(({ name: columnName }) => regex.test(columnName))
+        if (shouldExpand) {
+          expandedNodeKeys.add(`${dbName}_${tableName}`)
+          expandedNodeKeys.add(`${dbName}`)
+        }
+      })
+    }
+    this.setState({
+      filterKeyword,
+      autoExpandTable: true,
+      expandedNodeKeys: Array.from(expandedNodeKeys)
+    })
+  }
+  // FIXED: sql 的改动会改变view,此组件依赖view,会进行多余的render,此处进行优化
+  public shouldComponentUpdate(nextProps: ISourceTableProps, nextState: ISourceTableStates) {
+    const {sources, schema, view: {name, description, sourceId}} = this.props
+    if (
+      !shallowEqual(nextState, this.state) ||
+      nextProps.sources !== sources ||
+      nextProps.schema !== schema ||
+      nextProps.view.name !== name ||
+      nextProps.view.description !== description ||
+      nextProps.view.sourceId !== sourceId
+    ) {
+      return true
+    }
+    return false
+  }
+
+  public render () {
+    const { view, sources, schema, onDatabaseSelect } = this.props
+    const { filterKeyword, expandedNodeKeys } = this.state
+    const { name: viewName, description: viewDesc, sourceId } = view
+
+    return (
+      <div className={Styles.sourceTable}>
+        <Row gutter={16}>
+          <Col span={24}>
+            <Input placeholder="名称" value={viewName} onChange={this.inputChange('name')} />
+          </Col>
+          <Col span={24}>
+            <Input placeholder="描述" value={viewDesc} onChange={this.inputChange('description')} />
+          </Col>
+          <Col span={24}>
+            <Select
+              showSearch
+              dropdownMatchSelectWidth={false}
+              placeholder="数据源"
+              style={{width: '100%'}}
+              value={sourceId}
+              onChange={this.selectSource}
+              filterOption={filterSelectOption}
+            >
+              {sources.map(({ id, name }) => (<Option key={id.toString()} value={id}>{name}</Option>))}
+            </Select>
+          </Col>
+          <Col span={24}>
+            <Search
+              placeholder="搜索表/字段名称"
+              value={filterKeyword}
+              onChange={this.filterKeywordChange}
+            />
+          </Col>
+        </Row>
+        <div className={Styles.tree}>
+          <Tree
+            showIcon
+            key={view.sourceId}
+            loadData={this.loadTreeData}
+            onSelect={this.treeNodeSelect}
+            onExpand={this.treeNodeExpand}
+            expandedKeys={expandedNodeKeys}
+          >
+            {this.renderTableColumns(sourceId, schema, filterKeyword, onDatabaseSelect)}
+          </Tree>
+        </div>
+      </div>
+    )
+  }
+}
+
+export default SourceTable

+ 110 - 0
app/containers/DataManagerView/components/SqlEditor.tsx

@@ -0,0 +1,110 @@
+/*
+ * <<
+ * 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 debounce from 'lodash/debounce'
+
+import CodeMirror from 'codemirror/lib/codemirror'
+import 'codemirror/lib/codemirror.css'
+import 'assets/override/codemirror_theme.css'
+import 'codemirror/addon/hint/show-hint.css'
+import 'codemirror/addon/edit/matchbrackets'
+import 'codemirror/mode/sql/sql'
+import 'codemirror/addon/hint/show-hint'
+import 'codemirror/addon/hint/sql-hint'
+import 'codemirror/addon/display/placeholder'
+
+import Styles from '../View.less'
+
+interface ISqlEditorProps {
+  hints: {
+    [name: string]: []
+  }
+  value: string
+  onSqlChange: (sql: string) => void
+  onSqlEnter: () => void
+}
+
+export class SqlEditor extends React.PureComponent<ISqlEditorProps> {
+
+  private sqlEditorContainer = React.createRef<HTMLTextAreaElement>()
+  private sqlEditor
+  private debouncedSqlChange = debounce((val: string) => { this.props.onSqlChange(val) }, 500)
+
+  constructor (props) {
+    super(props)
+  }
+
+  public componentDidMount () {
+    this.initEditor(CodeMirror, this.props.value)
+  }
+
+  public componentDidUpdate () {
+    if (this.sqlEditor) {
+      const { value } = this.props
+      const localValue = this.sqlEditor.doc.getValue()
+      if (value !== localValue) {
+        this.sqlEditor.doc.setValue(this.props.value)
+      }
+    }
+  }
+
+  private initEditor = (codeMirror, value: string) => {
+    const { fromTextArea } = codeMirror
+    const config = {
+      mode: 'text/x-sql',
+      theme: '3024-day',
+      lineNumbers: true,
+      lineWrapping: false,
+      autoCloseBrackets: true,
+      matchBrackets: true,
+      foldGutter: true,
+      extraKeys: {
+        'Cmd-Enter': () => { this.props.onSqlEnter() },
+        'Ctrl-Enter': () => { this.props.onSqlEnter() }
+      }
+    }
+    this.sqlEditor = fromTextArea(this.sqlEditorContainer.current, config)
+    this.sqlEditor.doc.setValue(value)
+    this.sqlEditor.on('change', (_: CodeMirror.Editor, change: CodeMirror.EditorChange) => {
+      this.debouncedSqlChange(_.getDoc().getValue())
+
+      if (change.origin === '+input'
+          && change.text[0] !== ';'
+          && change.text[0].trim() !== ''
+          && change.text[1] !== '') {
+        this.sqlEditor.showHint({
+          completeSingle: false,
+          tables: this.props.hints
+        })
+      }
+    })
+  }
+
+  public render () {
+    return (
+      <div className={Styles.sqlEditor}>
+        <textarea ref={this.sqlEditorContainer} />
+      </div>
+    )
+  }
+}
+
+export default SqlEditor

+ 260 - 0
app/containers/DataManagerView/components/SqlEditorByAce.tsx

@@ -0,0 +1,260 @@
+/*
+ * <<
+ * 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, { useRef, useEffect, useCallback, useMemo } from 'react'
+import AceEditor, { IAceOptions } from 'react-ace'
+import languageTools from 'ace-builds/src-min-noconflict/ext-language_tools'
+import 'ace-builds/src-min-noconflict/ext-searchbox'
+import 'ace-builds/src-min-noconflict/theme-sqlserver'
+import 'ace-builds/src-min-noconflict/mode-sql'
+import ReactAce, { IAceEditorProps } from 'react-ace/lib/ace'
+import { debounce } from 'lodash'
+import { DEFAULT_FONT_SIZE } from 'app/globalConstants'
+import Styles from '../View.less'
+
+type TMode =
+  | 'sql'
+  | 'mysql'
+  | 'sqlserver'
+
+type TTheme =
+  | 'ambiance'
+  | 'chaos'
+  | 'chrome'
+  | 'clouds'
+  | 'dawn'
+  | 'eclipse'
+  | 'github'
+  | 'kuroir'
+  | 'terminal'
+  | 'textmate'
+  | 'tomorrow'
+  | 'twilight'
+  | 'xcode'
+  | 'sqlserver'
+
+enum EHintMeta {
+  table = 'table',
+  variable = 'variable',
+  column = 'column'
+}
+const THEME_DEFAULT = 'sqlserver'
+const MODE_DEFAULT = 'sql'
+const EDITOR_OPTIONS: IAceOptions = {
+  behavioursEnabled: true,
+  enableSnippets: false,
+  enableBasicAutocompletion: true,
+  enableLiveAutocompletion: true,
+  autoScrollEditorIntoView: true,
+  wrap: true,
+  useWorker: false
+}
+export interface ISqlEditorProps {
+  hints: { [name: string]: string[] }
+  value: string
+  /**
+   * 需引入对应的包 'ace-builds/src-min-noconflict/mode-${mode}'
+   */
+  mode?: TMode
+  /**
+   * 需引入对应的包 'ace-builds/src-min-noconflict/theme-${theme}'
+   */
+  theme?: TTheme
+  editorConfig?: IAceEditorProps
+  sizeChanged?: number
+  onSqlChange: (sql: string) => void
+  onSelect?: (sql: string) => void
+  onCmdEnter?: () => void
+}
+
+/**
+ * Editor Component
+ * @param props ISqlEditorProps
+ */
+function SqlEditor (props: ISqlEditorProps) {
+
+  const refEditor = useRef<ReactAce>()
+
+  const {
+    hints,
+    value,
+    mode = MODE_DEFAULT,
+    theme = THEME_DEFAULT,
+    sizeChanged,
+    editorConfig,
+    onSqlChange,
+    onSelect,
+    onCmdEnter
+  } = props
+
+  const resize = useCallback(debounce(() => {
+    refEditor.current.editor.resize()
+  }, 300), [])
+
+  const change = useCallback((sql: string) => {
+    onSqlChange(sql)
+  }, [])
+
+  const selectionChange = useCallback(debounce((selection: any) => {
+    const rawSelectedQueryText = refEditor.current.editor.session.doc.getTextRange(selection.getRange())
+    const selectedQueryText = rawSelectedQueryText.length > 1 ? rawSelectedQueryText : null
+    onSelect?.(selectedQueryText)
+  }, 300), [])
+
+  const commands = useMemo(() => [
+    {
+      name: 'execute',
+      bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
+      exec: onCmdEnter
+    }
+  ], [])
+
+  useEffect(() => {
+    resize()
+  }, [sizeChanged])
+
+  useEffect(() => {
+    setHintsPopover(hints)
+  }, [hints])
+
+  return (
+    <div className={Styles.sqlEditor}>
+      <AceEditor
+        ref={refEditor}
+        name="aceEditor"
+        width="100%"
+        height="100%"
+        fontSize={DEFAULT_FONT_SIZE}
+        mode={mode}
+        theme={theme}
+        value={value}
+        showPrintMargin={false}
+        highlightActiveLine={true}
+        setOptions={EDITOR_OPTIONS}
+        commands={commands}
+        onChange={change}
+        onSelectionChange={selectionChange}
+        {...editorConfig}
+      />
+    </div>
+  )
+}
+
+
+interface ICompleters {
+  value: string
+  name?: string
+  caption?: string
+  meta?: string
+  type?: string
+  score?: number
+}
+
+function setHintsPopover (hints: ISqlEditorProps['hints']) {
+  const {
+    textCompleter,
+    keyWordCompleter,
+    // snippetCompleter,
+    setCompleters
+  } = languageTools
+  const customHintsCompleter = {
+    identifierRegexps: [/[a-zA-Z_0-9.\-\u00A2-\uFFFF]/],
+    getCompletions: (editor, session, pos, prefix, callback) => {
+      const { tableKeywords, tableColumnKeywords, variableKeywords, columns } = formatCompleterFromHints(hints)
+      if (prefix[prefix.length - 1] === '.') {
+        const tableName = prefix.substring(0, prefix.length - 1)
+        const AliasTableColumnKeywords = genAliasTableColumnKeywords(editor, tableName, hints)
+        const hintList = tableKeywords.concat(variableKeywords, AliasTableColumnKeywords, tableColumnKeywords[tableName] || [])
+        return callback(null, hintList)
+      }
+      callback(null, tableKeywords.concat(variableKeywords, columns))
+    }
+  }
+  const completers = [
+    textCompleter,
+    keyWordCompleter,
+    // snippetCompleter,
+    customHintsCompleter
+  ]
+  setCompleters(completers)
+}
+
+function formatCompleterFromHints (hints: ISqlEditorProps['hints']) {
+  const variableKeywords: ICompleters[] = []
+  const tableKeywords: ICompleters[] = []
+  const tableColumnKeywords: { [tableName: string]: ICompleters[] } = {}
+  const columns: ICompleters[] = []
+  let score = 1000
+  Object.keys(hints).forEach((key) => {
+    const meta: EHintMeta = isVariable(key)
+    if (!meta) {
+      const { columnWithTableName, column } = genTableColumnKeywords(hints[key], key)
+      tableColumnKeywords[key] = columnWithTableName
+      columns.push(...column)
+      tableKeywords.push({ name: key, value: key, score: score--, meta: isTable() })
+    } else {
+      variableKeywords.push({ score: score--, value: key, meta })
+    }
+  })
+
+  return { tableKeywords, tableColumnKeywords, variableKeywords, columns }
+}
+
+function genTableColumnKeywords (table: string[], tableName: string) {
+  let score = 100
+  const columnWithTableName: ICompleters[] = []
+  const column: ICompleters[] = []
+  table.forEach((columnVal) => {
+    const basis = { score: score--, meta: isColumn() }
+    columnWithTableName.push({
+      caption: `${tableName}.${columnVal}`,
+      name: `${tableName}.${columnVal}`,
+      value: `${tableName}.${columnVal}`,
+      ...basis
+    })
+    column.push({ value: columnVal, name: columnVal, ...basis })
+  })
+  return { columnWithTableName, column }
+}
+
+function genAliasTableColumnKeywords (editor, aliasTableName: string, hints: ISqlEditorProps['hints']) {
+  const content = editor.getSession().getValue()
+  const tableName = Object.keys(hints).find((tableName) => {
+    const reg = new RegExp(`.+${tableName}\\s*(as|AS)?(?=\\s+${aliasTableName}\\s*)`, 'im')
+    return reg.test(content)
+  })
+  if (!tableName) { return [] }
+  const { columnWithTableName } = genTableColumnKeywords(hints[tableName], aliasTableName)
+  return columnWithTableName
+}
+
+function isVariable (key: string) {
+  return key.startsWith('$') && key.endsWith('$') && EHintMeta.variable
+}
+
+function isTable (key?: string) {
+  return EHintMeta.table
+}
+
+function isColumn (key?: string) {
+  return EHintMeta.column
+}
+
+export default SqlEditor

+ 127 - 0
app/containers/DataManagerView/components/SqlPreview.tsx

@@ -0,0 +1,127 @@
+import React from 'react'
+import { findDOMNode } from 'react-dom'
+import memoizeOne from 'memoize-one'
+
+import { Table } from 'antd'
+import { ColumnProps, TableProps } from 'antd/lib/table'
+import { PaginationConfig } from 'antd/lib/pagination'
+import Styles from '../View.less'
+
+import { IExecuteSqlResponse, ISqlColumn } from '../types'
+import { DEFAULT_SQL_PREVIEW_PAGE_SIZE, SQL_PREVIEW_PAGE_SIZE_OPTIONS } from '../constants'
+import { getTextWidth } from 'utils/util'
+
+export interface ISqlPreviewProps {
+  loading: boolean
+  response: IExecuteSqlResponse
+  height?: number
+  size: TableProps<any>['size']
+}
+
+interface ISqlPreviewStates {
+  tableBodyHeight: number
+}
+
+export class SqlPreview extends React.PureComponent<ISqlPreviewProps, ISqlPreviewStates> {
+
+  private static readonly TableCellPaddingWidth = 8
+  private static readonly TableCellMaxWidth = 300
+
+  private static ExcludeElems = ['.ant-table-thead', '.ant-pagination.ant-table-pagination']
+
+  private static basePagination: PaginationConfig = {
+    pageSize: DEFAULT_SQL_PREVIEW_PAGE_SIZE,
+    pageSizeOptions: SQL_PREVIEW_PAGE_SIZE_OPTIONS.map((size) => size.toString()),
+    showQuickJumper: true,
+    showSizeChanger: true
+  }
+
+  private prepareTable = memoizeOne((columns: ISqlColumn[], resultList: any[]) => {
+    const rowKey = `rowKey_${new Date().getTime()}`
+    resultList.forEach((record, idx) => record[rowKey] = Object.values(record).join('_') + idx)
+
+    const tableColumns = columns.map<ColumnProps<any>>((col) => {
+      const width = SqlPreview.computeColumnWidth(resultList, col.name)
+      return {
+        title: col.name,
+        dataIndex: col.name,
+        width
+      }
+    })
+    return { tableColumns, rowKey }
+  })
+
+  private static computeColumnWidth = (resultList: any[], columnName: string) => {
+    let textList = resultList.map((item) => item[columnName])
+    textList = textList.filter((text, idx) => textList.indexOf(text) === idx)
+    const contentMaxWidth = textList.reduce((maxWidth, text) =>
+      Math.max(maxWidth, getTextWidth(text, '700', '14px')), -Infinity)
+    const titleWidth = getTextWidth(columnName, '500', '14px')
+    let maxWidth = Math.max(contentMaxWidth, titleWidth) + (2 * SqlPreview.TableCellPaddingWidth) + 2
+    maxWidth = Math.min(maxWidth, SqlPreview.TableCellMaxWidth)
+    return maxWidth
+  }
+
+  private table = React.createRef<Table<any>>()
+  public state: Readonly<ISqlPreviewStates> = { tableBodyHeight: 0 }
+
+  public componentDidMount () {
+    const tableBodyHeight = this.computeTableBody()
+    this.setState({ tableBodyHeight })
+  }
+
+  public componentDidUpdate () {
+    const newTableBodyHeight = this.computeTableBody()
+    if (Math.abs(newTableBodyHeight - this.state.tableBodyHeight) > 5) { // FIXED table body compute vibration
+      this.setState({ tableBodyHeight: newTableBodyHeight })
+    }
+  }
+
+  private computeTableBody = () => {
+    const tableDom = findDOMNode(this.table.current) as Element
+    if (!tableDom) { return 0 }
+    const excludeElemsHeight = SqlPreview.ExcludeElems.reduce((acc, exp) => {
+      const elem = tableDom.querySelector(exp)
+      if (!elem) { return acc }
+      const style = window.getComputedStyle(elem)
+      const { marginTop, marginBottom } = style
+      const height = elem.clientHeight + parseInt(marginTop, 10) + parseInt(marginBottom, 10)
+      return acc + height
+    }, 0)
+    const tableBodyHeight = this.props.height - excludeElemsHeight
+    return tableBodyHeight
+  }
+
+  public render () {
+    const { loading, response, size } = this.props
+    const { totalCount, columns = [], resultList =[] } = response
+    const paginationConfig: PaginationConfig = {
+      ...SqlPreview.basePagination,
+      total: totalCount
+
+    }
+    const { tableColumns, rowKey } = this.prepareTable(columns, resultList)
+    const scroll: TableProps<any>['scroll'] = {
+      x: tableColumns.reduce((acc, col) => (col.width as number + acc), 0),
+      y: this.state.tableBodyHeight
+    }
+
+    return (
+      <Table
+        ref={this.table}
+        className={Styles.sqlPreview}
+        bordered
+        size={size}
+        pagination={paginationConfig}
+        dataSource={resultList}
+        columns={tableColumns}
+        scroll={scroll}
+        loading={loading}
+        rowKey={rowKey}
+      />
+    )
+  }
+
+}
+
+export default SqlPreview

+ 328 - 0
app/containers/DataManagerView/components/VariableModal.tsx

@@ -0,0 +1,328 @@
+import React from 'react'
+import { Modal, Form, Input, Select, Checkbox, Button, Row, Col } from 'antd'
+const FormItem = Form.Item
+const TextArea = Input.TextArea
+const { Option } = Select
+import { CheckboxChangeEvent } from 'antd/lib/checkbox'
+import { FormComponentProps } from 'antd/lib/form/Form'
+import ConditionValuesControl, { ConditionValueTypes } from 'components/ConditionValuesControl'
+import {
+  IViewVariable,
+  IDacChannel, IDacTenant, IDacBiz
+} from 'containers/View/types'
+import OperatorTypes from 'utils/operatorTypes'
+import { ViewVariableTypes, ViewVariableTypesLocale, ViewVariableValueTypes, ViewVariableValueTypesLocale } from 'containers/View/constants'
+
+export interface IVariableModalProps {
+  visible?: boolean
+  variable?: IViewVariable
+
+  channels: IDacChannel[]
+  tenants: IDacTenant[]
+  bizs: IDacBiz[]
+
+  nameValidator?: (key: string, name: string, callback: (msg?: string) => void) => void
+  onCancel?: () => void
+  onSave?: (variable: IViewVariable) => void
+
+  onLoadDacTenants: (channelName: string) => void
+  onLoadDacBizs: (channelName: string, tenantId: number) => void
+}
+
+interface IVariableModalStates {
+  operatorType: OperatorTypes
+  selectedType: ViewVariableTypes
+  selectedValueType: ViewVariableValueTypes
+  defaultValues: ConditionValueTypes[]
+  isUdf: boolean
+  isFromService: boolean
+}
+
+const defaultVarible: IViewVariable = {
+  key: '',
+  name: '',
+  alias: '',
+  type: ViewVariableTypes.Query,
+  valueType: ViewVariableValueTypes.String,
+  defaultValues: [],
+  udf: false,
+  fromService: false
+}
+
+export class VariableModal extends React.Component<IVariableModalProps & FormComponentProps, IVariableModalStates> {
+
+  private formItemStyle = {
+    labelCol: { span: 6 },
+    wrapperCol: { span: 18 }
+  }
+
+  private viewVariableTypeOptions = Object.entries(ViewVariableTypesLocale).map(([variableType, text]) => (
+    <Option key={variableType} value={variableType}>{text}</Option>
+  ))
+
+  private viewVariableValueTypeOptions = Object.entries(ViewVariableValueTypesLocale).map(([valueType, text]) => (
+    <Option key={valueType} value={valueType}>{text}</Option>
+  ))
+
+  public state: Readonly<IVariableModalStates> = {
+    operatorType: OperatorTypes.In,
+    selectedType: ViewVariableTypes.Query,
+    selectedValueType: ViewVariableValueTypes.String,
+    defaultValues: [],
+    isUdf: false,
+    isFromService: false
+  }
+
+  public componentDidUpdate (prevProps: IVariableModalProps & FormComponentProps) {
+    const { form, variable, visible, channels } = this.props
+    if (variable !== prevProps.variable || visible !== prevProps.visible) {
+      const { type, valueType, defaultValues, udf, fromService, channel } = variable || defaultVarible
+      if (channel && visible) {
+        const { name: channelName, tenantId } = channel
+        const { onLoadDacTenants, onLoadDacBizs } = this.props
+        onLoadDacTenants(channelName)
+        onLoadDacBizs(channelName, tenantId)
+      }
+      this.setState({
+        selectedType: type,
+        selectedValueType: valueType,
+        defaultValues: defaultValues || [],
+        isUdf: udf,
+        isFromService: fromService && channels.length > 0
+      }, () => {
+        form.setFieldsValue(variable || defaultVarible)
+      })
+    }
+  }
+
+  private typeChange = (selectedType: ViewVariableTypes) => {
+    this.setState(({ isUdf, isFromService }) => ({
+      selectedType,
+      isUdf: selectedType === ViewVariableTypes.Authorization ? false : isUdf,
+      isFromService: selectedType === ViewVariableTypes.Query ? false : isFromService
+    }))
+  }
+
+  private valueTypeChange = (selectedValueType: ViewVariableValueTypes) => {
+    this.setState({
+      selectedValueType,
+      operatorType: selectedValueType === ViewVariableValueTypes.Boolean ? OperatorTypes.Equal : OperatorTypes.In,
+      defaultValues: []
+    })
+  }
+
+  private singleDefaultValuesChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+    this.defaultValueChange([e.target.value])
+  }
+
+  private defaultValueChange = (values: ConditionValueTypes[]) => {
+    this.setState({ defaultValues: values })
+  }
+
+  private udfChange = (e: CheckboxChangeEvent) => {
+    const udf = e.target.checked
+    this.setState({ isUdf: udf })
+    this.defaultValueChange([])
+  }
+
+  private fromServiceChange = (e: CheckboxChangeEvent) => {
+    const fromService = e.target.checked
+    this.setState({ isFromService: fromService })
+  }
+
+  private loadDacBizs = (tenantId: number) => {
+    const { form, onLoadDacBizs } = this.props
+    const channelName = form.getFieldValue('channel.name')
+    onLoadDacBizs(channelName, tenantId)
+  }
+
+  private validateVariableName = (_: any, name: string, callback: (msg?: string) => void) => {
+    const isValidName = /^[\w]+$/.test(name)
+    if (!isValidName) {
+      callback('变量名称由字母、数字及下划线组成')
+      return
+    }
+    const { nameValidator } = this.props
+    if (!nameValidator) {
+      callback()
+      return
+    }
+    const { variable } = this.props
+    const key = variable ? variable.key : ''
+    nameValidator(key, name, callback)
+  }
+
+  private clearFieldsValue = () => {
+    this.props.form.resetFields()
+  }
+
+  private save = () => {
+    const { form, variable, onSave } = this.props
+    form.validateFieldsAndScroll((err, fieldsValue) => {
+      if (!err) {
+        const updatedVariable = fieldsValue as IViewVariable
+        if (variable) {
+          updatedVariable.key = variable.key
+        }
+        if (updatedVariable.type === ViewVariableTypes.Query) {
+          updatedVariable.defaultValues = this.state.defaultValues
+        }
+        console.log(updatedVariable)
+        // return
+        onSave(updatedVariable)
+      }
+    })
+  }
+
+  public render () {
+    const {
+      visible, variable, onCancel, form,
+      channels, tenants, bizs,
+      onLoadDacTenants
+    } = this.props
+    const { getFieldDecorator } = form
+    const { operatorType, selectedType, selectedValueType, defaultValues, isUdf, isFromService } = this.state
+
+    const modalButtons = [(
+      <Button
+        key="back"
+        size="large"
+        onClick={onCancel}
+      >
+        取 消
+      </Button>
+    ), (
+      <Button
+        key="submit"
+        size="large"
+        type="primary"
+        onClick={this.save}
+      >
+        保 存
+      </Button>
+    )]
+
+    return (
+      <Modal
+        title={`${variable && variable.key ? '修改' : '新增'}变量`}
+        wrapClassName="ant-modal-small"
+        maskClosable={false}
+        visible={visible}
+        footer={modalButtons}
+        onCancel={onCancel}
+        afterClose={this.clearFieldsValue}
+      >
+        <Form>
+          <FormItem label="名称" {...this.formItemStyle}>
+            {getFieldDecorator<IViewVariable>('name', {
+              rules: [{
+                required: true,
+                validator: this.validateVariableName
+              }]
+            })(<Input />)}
+          </FormItem>
+          <FormItem label="别名" {...this.formItemStyle}>
+            {getFieldDecorator<IViewVariable>('alias')(<Input />)}
+          </FormItem>
+          <FormItem label="类型" {...this.formItemStyle}>
+            {getFieldDecorator<IViewVariable>('type', {
+              rules: [{
+                required: true,
+                message: '请选择类型'
+              }]
+            })(<Select onChange={this.typeChange}>{this.viewVariableTypeOptions}</Select>)}
+          </FormItem>
+          <FormItem label="值类型" {...this.formItemStyle}>
+            {getFieldDecorator<IViewVariable>('valueType', {
+              rules: [{
+                required: true,
+                message: '请选择值类型'
+              }]
+            })(<Select onChange={this.valueTypeChange}>{this.viewVariableValueTypeOptions}</Select>)}
+          </FormItem>
+          {selectedType === ViewVariableTypes.Query && selectedValueType !== ViewVariableValueTypes.SqlExpression && (
+            <>
+              <FormItem>
+                <Row>
+                  <Col span={this.formItemStyle.wrapperCol.span} offset={this.formItemStyle.labelCol.span}>
+                    {getFieldDecorator<IViewVariable>('udf', {
+                      valuePropName: 'checked',
+                      initialValue: isUdf
+                    })(
+                      <Checkbox onChange={this.udfChange}>使用表达式</Checkbox>
+                    )}
+                  </Col>
+                </Row>
+              </FormItem>
+              {!isUdf && <FormItem label="默认值" {...this.formItemStyle}>
+                <ConditionValuesControl
+                  visualType={selectedValueType}
+                  operatorType={operatorType}
+                  conditionValues={defaultValues}
+                  onChange={this.defaultValueChange}
+                />
+              </FormItem>}
+            </>
+          )}
+          {selectedType === ViewVariableTypes.Query && (isUdf || selectedValueType === ViewVariableValueTypes.SqlExpression) && (
+            <FormItem label="表达式" {...this.formItemStyle}>
+              <TextArea placeholder="请输入表达式" value={defaultValues[0] as string} onChange={this.singleDefaultValuesChange} rows={3} />
+            </FormItem>
+          )}
+          {selectedType === ViewVariableTypes.Authorization && channels.length > 0 && (
+            <FormItem>
+              <Row>
+                <Col span={this.formItemStyle.wrapperCol.span} offset={this.formItemStyle.labelCol.span}>
+                  {getFieldDecorator<IViewVariable>('fromService', {
+                    valuePropName: 'checked'
+                  })(
+                    <Checkbox onChange={this.fromServiceChange}>通过外部服务取值变量</Checkbox>)}
+                </Col>
+              </Row>
+            </FormItem>
+          )}
+          {isFromService && (
+            <>
+              <FormItem label="服务" {...this.formItemStyle}>
+                {getFieldDecorator('channel.name', {
+                  rules: [{
+                    required: true,
+                    message: '服务不能为空'
+                  }]
+                })(
+                <Select onChange={onLoadDacTenants}>
+                  {channels.map((c) => <Option key={c} value={c}>{c}</Option>)}
+                </Select>)}
+              </FormItem>
+              <FormItem label="租户" {...this.formItemStyle}>
+                {getFieldDecorator('channel.tenantId', {
+                  rules: [{
+                    required: true,
+                    message: '租户不能为空'
+                  }]
+                })(
+                <Select onChange={this.loadDacBizs}>
+                  {tenants.map(({ id, name }) => <Option key={id.toString()} value={id}>{name}</Option>)}
+                </Select>)}
+              </FormItem>
+              <FormItem label="业务" {...this.formItemStyle}>
+                {getFieldDecorator('channel.bizId', {
+                  rules: [{
+                    required: true,
+                    message: '业务不能为空'
+                  }]
+                })(
+                <Select>
+                  {bizs.map(({ id, name }) => <Option key={id.toString()} value={id}>{name}</Option>)}
+                </Select>)}
+              </FormItem>
+            </>
+          )}
+        </Form>
+      </Modal>
+    )
+  }
+
+}
+
+export default Form.create<IVariableModalProps & FormComponentProps>()(VariableModal)

+ 77 - 0
app/containers/DataManagerView/components/ViewVariableList.tsx

@@ -0,0 +1,77 @@
+import React from 'react'
+import { List, Icon, Tooltip, Popconfirm, Tag } from 'antd'
+
+import { IViewVariable } from 'containers/View/types'
+import { ViewVariableTypes } from '../constants'
+
+import Styles from '../View.less'
+
+export interface IViewVariableListProps {
+  variables: IViewVariable[]
+  className?: string
+  onAdd?: () => void
+  onDelete?: (key: string) => void
+  onEdit?: (variable: IViewVariable) => void
+}
+
+export class ViewVariableList extends React.PureComponent<IViewVariableListProps> {
+
+  private editItem = (variable: IViewVariable) => () => {
+    this.props.onEdit({ ...variable })
+  }
+
+  private deleteItem = (key: string) => () => {
+    this.props.onDelete(key)
+  }
+
+  private renderItem = (item: IViewVariable) => {
+    const icons = [
+      (
+        <Tooltip key="edit" title="修改">
+          <Icon onClick={this.editItem(item)} type="edit" />
+        </Tooltip>
+      ),
+      (
+        <Popconfirm
+          key="delete"
+          title="确定删除?"
+          placement="left"
+          onConfirm={this.deleteItem(item.key)}
+        >
+          <Tooltip title="删除">
+            <Icon type="delete" />
+          </Tooltip>
+        </Popconfirm>
+      )
+    ]
+    const { name, alias, type } = item
+    const text = alias ? `${name}[${alias}]` : `${name}`
+    const color = type === ViewVariableTypes.Query ? 'green' : 'volcano'
+    const category = type === ViewVariableTypes.Query ? 'QUERY' : 'AUTH'
+
+    return (
+      <List.Item actions={icons}>
+        <Tag color={color}>{category}</Tag>
+        <div className={Styles.variableItem}>{text}</div>
+      </List.Item>
+    )
+  }
+
+  public render () {
+    const { variables, className, onAdd } = this.props
+
+    return (
+      <List
+        className={className}
+        size="small"
+        header={<div className={Styles.viewVariableHeader}><h4>变量</h4><Icon type="plus" onClick={onAdd} title="添加" /></div>}
+        locale={{ emptyText: '暂无变量' }}
+        dataSource={variables}
+        renderItem={this.renderItem}
+      />
+    )
+  }
+
+}
+
+export default ViewVariableList

+ 166 - 0
app/containers/DataManagerView/constants.ts

@@ -0,0 +1,166 @@
+/*
+ * <<
+ * 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 { createTypes } from 'utils/redux'
+import { SQL_STRING_TYPES, SQL_NUMBER_TYPES, SQL_DATE_TYPES, SQL_TYPES } from 'app/globalConstants'
+
+enum Types {
+  LOAD_VIEWS = 'davinci/View/LOAD_VIEWS',
+  LOAD_VIEWS_SUCCESS = 'davinci/View/LOAD_VIEWS_SUCCESS',
+  LOAD_VIEWS_FAILURE = 'davinci/View/LOAD_VIEWS_FAILURE',
+
+  LOAD_VIEWS_DETAIL = 'davinci/View/LOAD_VIEWS_DETAIL',
+  LOAD_VIEWS_DETAIL_SUCCESS = 'davinci/View/LOAD_VIEWS_DETAIL_SUCCESS',
+  LOAD_VIEWS_DETAIL_FAILURE = 'davinci/View/LOAD_VIEWS_DETAIL_FAILURE',
+
+  ADD_VIEW = 'davinci/View/ADD_VIEW',
+  ADD_VIEW_SUCCESS = 'davinci/View/ADD_VIEW_SUCCESS',
+  ADD_VIEW_FAILURE = 'davinci/View/ADD_VIEW_FAILURE',
+
+  DELETE_VIEW = 'davinci/View/DELETE_VIEW',
+  DELETE_VIEW_SUCCESS = 'davinci/View/DELETE_VIEW_SUCCESS',
+  DELETE_VIEW_FAILURE = 'davinci/View/DELETE_VIEW_FAILURE',
+
+  EDIT_VIEW = 'davinci/View/EDIT_VIEW',
+  EDIT_VIEW_SUCCESS = 'davinci/View/EDIT_VIEW_SUCCESS',
+  EDIT_VIEW_FAILURE = 'davinci/View/EDIT_VIEW_FAILURE',
+
+  COPY_VIEW = 'davinci/View/COPY_VIEW',
+  COPY_VIEW_SUCCESS = 'davinci/View/COPY_VIEW_SUCCESS',
+  COPY_VIEW_FAILURE = 'davinci/View/COPY_VIEW_FAILURE',
+
+  EXECUTE_SQL = 'davinci/View/EXECUTE_SQL',
+  EXECUTE_SQL_SUCCESS = 'davinci/View/EXECUTE_SQL_SUCCESS',
+  EXECUTE_SQL_FAILURE = 'davinci/View/EXECUTE_SQL_FAILURE',
+  EXECUTE_SQL_CANCEL = 'davinci/View/EXECUTE_SQL_CANCEL',
+  IS_LAST_EXECUTE_WHOLE_SQL = 'davinci/View/IS_LAST_EXECUTE_WHOLE_SQL',
+
+  UPDATE_EDITING_VIEW = 'davinci/View/UPDATE_EDITING_VIEW',
+  UPDATE_EDITING_VIEW_INFO = 'davinci/View/UPDATE_EDITING_VIEW_INFO',
+
+  SET_SQL_LIMIT = 'davinci/View/SET_SQL_LIMIT',
+  RESET_VIEW_STATE = 'davinci/View/RESET_VIEW_STATE',
+
+  /** Actions for fetch external authorization variables values */
+  LOAD_DAC_CHANNELS = 'davinci/View/LOAD_DAC_CHANNELS',
+  LOAD_DAC_CHANNELS_SUCCESS = 'davinci/View/LOAD_DAC_CHANNELS_SUCCESS',
+  LOAD_DAC_CHANNELS_FAILURE = 'davinci/View/LOAD_DAC_CHANNELS_FAILURE',
+
+  LOAD_DAC_TENANTS = 'davinci/View/LOAD_DAC_TENANTS',
+  LOAD_DAC_TENANTS_SUCCESS = 'davinci/View/LOAD_DAC_TENANTS_SUCCESS',
+  LOAD_DAC_TENANTS_FAILURE = 'davinci/View/LOAD_DAC_TENANTS_FAILURE',
+
+  LOAD_DAC_BIZS = 'davinci/View/LOAD_DAC_BIZS',
+  LOAD_DAC_BIZS_SUCCESS = 'davinci/View/LOAD_DAC_BIZS_SUCCESS',
+  LOAD_DAC_BIZS_FAILURE = 'davinci/View/LOAD_DAC_BIZS_FAILURE',
+  /** */
+
+  /** Actions for external usages */
+  LOAD_SELECT_OPTIONS = 'davinci/View/LOAD_SELECT_OPTIONS',
+  LOAD_SELECT_OPTIONS_SUCCESS = 'davinci/View/LOAD_SELECT_OPTIONS_SUCCESS',
+  LOAD_SELECT_OPTIONS_FAILURE = 'davinci/View/LOAD_SELECT_OPTIONS_FAILURE',
+
+  LOAD_VIEW_DATA = 'davinci/View/LOAD_VIEW_DATA',
+  LOAD_VIEW_DATA_SUCCESS = 'davinci/View/LOAD_VIEW_DATA_SUCCESS',
+  LOAD_VIEW_DATA_FAILURE = 'davinci/View/LOAD_VIEW_DATA_FAILURE',
+
+  LOAD_COLUMN_DISTINCT_VALUE = 'davinci/View/LOAD_COLUMN_DISTINCT_VALUE',
+
+  LOAD_VIEW_DATA_FROM_VIZ_ITEM = 'davinci/View/LOAD_VIEW_DATA_FROM_VIZ_ITEM',
+  LOAD_VIEW_DATA_FROM_VIZ_ITEM_SUCCESS = 'davinci/View/LOAD_VIEW_DATA_FROM_VIZ_ITEM_SUCCESS',
+  LOAD_VIEW_DATA_FROM_VIZ_ITEM_FAILURE = 'davinci/View/LOAD_VIEW_DATA_FROM_VIZ_ITEM_FAILURE'
+  /**  */
+}
+
+export const ActionTypes = createTypes(Types)
+
+export enum ViewVariableTypes {
+  Query = 'query',
+  Authorization = 'auth'
+}
+
+export const ViewVariableTypesLocale = {
+  [ViewVariableTypes.Query]: '查询变量',
+  [ViewVariableTypes.Authorization]: '权限变量'
+}
+
+export enum ViewVariableValueTypes {
+  String = 'string',
+  Number = 'number',
+  Boolean = 'boolean',
+  Date = 'date',
+  SqlExpression = 'sql'
+}
+
+export const ViewVariableValueTypesLocale = {
+  [ViewVariableValueTypes.String]: '字符串',
+  [ViewVariableValueTypes.Number]: '数字',
+  [ViewVariableValueTypes.Boolean]: '布尔',
+  [ViewVariableValueTypes.Date]: '日期',
+  [ViewVariableValueTypes.SqlExpression]: 'SQL表达式'
+}
+
+export enum ViewModelTypes {
+  Category = 'category',
+  Value = 'value'
+}
+
+export const DefaultModelTypeSqlTypeSetting = {
+  [ViewModelTypes.Value]: SQL_NUMBER_TYPES,
+
+  // except SQL_STRING_TYPES field
+  // SQL_NUMBER_TYPES and SQL_DATE_TYPES field can also take it as ViewModelTypes.Category
+  [ViewModelTypes.Category]: SQL_TYPES
+}
+
+export const ViewModelTypesLocale = {
+  [ViewModelTypes.Category]: '维度',
+  [ViewModelTypes.Value]: '指标'
+}
+
+export enum ViewModelVisualTypes {
+  Number = 'number',
+  String = 'string',
+  Date = 'date',
+  GeoCountry = 'geoCountry',
+  GeoProvince = 'geoProvince',
+  GeoCity = 'geoCity'
+}
+
+export const VisualTypeSqlTypeSetting = {
+  [ViewModelVisualTypes.Number]: SQL_NUMBER_TYPES,
+
+  // SQL_NUMBER_TYPES field can also take it as ViewModelVisualTypes.String
+  [ViewModelVisualTypes.String]: SQL_STRING_TYPES.concat(SQL_NUMBER_TYPES),
+  [ViewModelVisualTypes.Date]: SQL_DATE_TYPES
+}
+
+export const ViewModelVisualTypesLocale = {
+  [ViewModelVisualTypes.Number]: '数字',
+  [ViewModelVisualTypes.String]: '字符',
+  [ViewModelVisualTypes.Date]: '日期',
+  [ViewModelVisualTypes.GeoCountry]: '地理国家',
+  [ViewModelVisualTypes.GeoProvince]: '地理省份',
+  [ViewModelVisualTypes.GeoCity]: '地理城市'
+}
+
+export const DEFAULT_SQL_LIMIT = 500
+export const DEFAULT_SQL_PREVIEW_PAGE_SIZE = 100
+export const SQL_PREVIEW_PAGE_SIZE_OPTIONS = [100, 200, 500, 1000]

+ 70 - 0
app/containers/DataManagerView/index.less

@@ -0,0 +1,70 @@
+@import "~assets/less/variable";
+
+.treeTableContainer {
+  display: flex;
+}
+
+.treeContainer {
+  padding: 0 0 10px;
+  border: 1px solid rgb(232, 232, 232);
+  margin-bottom: 20px;
+  width: 240px;
+  margin-right: 10px;
+  border-radius: 4px 4px 0 0;
+
+
+  .treeTitle {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    line-height: 36px;
+    padding: 10px 10px;
+    background-color: rgb(250, 250, 250);;
+    border-bottom: 1px solid #e8e8e8;
+
+    h6, > div {
+      line-height: 21px;
+    }
+
+    .treePlusNode {
+      //padding: 0 4px;
+      cursor: pointer
+    }
+  }
+
+  .treeContent {
+    width: 100%;
+    padding: 10px 0 0;
+
+    .treeNode {
+      padding: 8px 10px;
+      margin: 2px 0;
+      display: flex;
+      justify-content: space-between;
+      cursor: pointer;
+      transition: all .2s;
+      border-radius: 4px;
+
+      &.treeNodeChild {
+        //margin-left: 20px
+      }
+
+      &.treeNodeSelected {
+        background-color: #d7d7d7 !important;
+        //color: #fff;
+      }
+
+      &:hover {
+        background-color: #fafafa;
+      }
+
+      .treeNodeLeft {
+        flex: 1;
+
+        > i {
+          padding-right: 6px;
+        }
+      }
+    }
+  }
+}

+ 594 - 0
app/containers/DataManagerView/index.tsx

@@ -0,0 +1,594 @@
+/*
+ * <<
+ * 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 { Link } from 'react-router-dom'
+import { RouteComponentWithParams } from 'utils/types'
+
+import injectReducer from 'utils/injectReducer'
+import injectSaga from 'utils/injectSaga'
+import reducer from './reducer'
+import sagas from './sagas'
+
+import { checkNameUniqueAction } from 'containers/App/actions'
+import { ViewActions, ViewActionType } from './actions'
+import { makeSelectViews, makeSelectLoading } from './selectors'
+import { makeSelectCurrentProject } from 'containers/Projects/selectors'
+
+import ModulePermission from '../Account/components/checkModulePermission'
+import { initializePermission } from '../Account/components/checkUtilPermission'
+
+import {
+  Table,
+  Tooltip,
+  Button,
+  Row,
+  Col,
+  Breadcrumb,
+  Icon,
+  Popconfirm,
+  message,
+  Tree,
+  Popover,
+  Dropdown,
+  Menu, Modal, Spin
+} from 'antd'
+import { ColumnProps, PaginationConfig, SorterResult } from 'antd/lib/table'
+import { ButtonProps } from 'antd/lib/button'
+import Container, { ContainerTitle, ContainerBody } from 'components/Container'
+import Box from 'components/Box'
+import SearchFilterDropdown from 'components/SearchFilterDropdown'
+import CopyModal from './components/CopyModal'
+import CatalogueModal from './components/CatalogueModal'
+
+import { IViewBase, IView, IViewLoading, ICatalogue } from './types'
+import { IProject } from '../Projects/types'
+
+import utilStyles from 'assets/less/util.less'
+import styles from './index.less'
+import request from 'utils/request'
+import api from 'utils/api'
+import classnames from 'classnames'
+
+interface IViewListStateProps {
+  views: IViewBase[]
+  currentProject: IProject
+  loading: IViewLoading
+}
+
+interface IViewListDispatchProps {
+  onLoadViews: (projectId: number, parentId: number) => void
+  onDeleteView: (viewId: number, resolve: () => void) => void
+  onCopyView: (view: IViewBase, resolve: () => void) => void
+  onCheckName: (data, resolve, reject) => void
+}
+
+type IViewListProps = IViewListStateProps & IViewListDispatchProps & RouteComponentWithParams
+
+// tslint:disable-next-line:interface-name
+interface Catalogue {
+  description?: string // 资源描述
+  extConfig?: string // 扩展信息
+  id?: number
+  industry?: string // 行业分类
+  name?: string // 资源名称
+  originDept?: string // 来源部门
+  originSystem?: string // 来源系统
+  parentId?: string
+  projectId?: number
+}
+
+interface IViewListStates {
+  screenWidth: number
+  tempFilterViewName: string
+  filterViewName: string
+  filterDropdownVisible: boolean
+  tableSorter: SorterResult<IViewBase>
+
+  copyModalVisible: boolean
+  copyFromView: IViewBase
+
+  viewList: IViewBase[]
+
+  catalogueModalVisible: boolean
+  catalogueFromView: ICatalogue
+  saveCatalogueLoading: boolean
+
+  catalogues: ICatalogue[]
+  selectedCatalogueKeys: string[]
+
+  treeLoading: boolean
+  tableLoading: boolean
+}
+
+const { TreeNode, DirectoryTree } = Tree
+
+export class ViewList extends React.PureComponent<IViewListProps, IViewListStates> {
+
+  public state: Readonly<IViewListStates> = {
+    screenWidth: document.documentElement.clientWidth,
+    tempFilterViewName: '',
+    filterViewName: '',
+    filterDropdownVisible: false,
+    tableSorter: null,
+
+    copyModalVisible: false,
+    copyFromView: null,
+
+    catalogueModalVisible: false,
+    catalogueFromView: null,
+    saveCatalogueLoading: false,
+    catalogues: [],
+
+    viewList: [],
+
+    treeLoading: false,
+    tableLoading: false
+  }
+
+  public async componentWillMount() {
+    const { projectId } = this.props.match.params
+
+    await (projectId && this.getCatalogues())
+    // const parentId = Number(this.state.selectedCatalogueKeys[0]) || null
+    // tslint:disable-next-line:no-unused-expression
+    projectId && this.loadViews()
+
+    window.addEventListener('resize', this.setScreenWidth, false)
+  }
+
+  private loadViews = async() => {
+    const { projectId } = this.props.match.params
+    if (projectId && this.state.selectedCatalogueKeys.length > 0) {
+      const parentId = Number(this.state.selectedCatalogueKeys[0])
+      try {
+        this.setState({ tableLoading: true })
+        const data = await request(api.getViewsByParentId + `?projectId=${projectId}&parentId=${parentId}`, { method: 'get' })
+        this.setState({ viewList: data.payload as unknown as IViewBase[] ?? [] })
+      } catch (e) {
+        console.log(e)
+      } finally {
+        this.setState({ tableLoading: false })
+      }
+    }
+  }
+
+  public componentWillUnmount() {
+    window.removeEventListener('resize', this.setScreenWidth, false)
+  }
+
+  private setScreenWidth = () => {
+    this.setState({ screenWidth: document.documentElement.clientWidth })
+  }
+
+  private getFilterViews = memoizeOne((viewName: string, views: IViewBase[]) => {
+    if (!Array.isArray(views) || !views.length) {
+      return []
+    }
+    const regex = new RegExp(viewName, 'gi')
+    const filterViews = views.filter((v) => v.name.match(regex) || v.description.match(regex))
+    return filterViews
+  })
+
+  private static getViewPermission = memoizeOne((project: IProject) => ({
+    viewPermission: initializePermission(project, 'viewPermission'),
+    AdminButton: ModulePermission<ButtonProps>(project, 'view', true)(Button),
+    EditButton: ModulePermission<ButtonProps>(project, 'view', false)(Button)
+  }))
+
+  private getTableColumns = (
+    { viewPermission, AdminButton, EditButton }: ReturnType<typeof ViewList.getViewPermission>
+  ) => {
+    // const { views } = this.props
+    const { viewList } = this.state
+    const { tempFilterViewName, filterViewName, filterDropdownVisible, tableSorter } = this.state
+    const sourceNames = viewList.map(({ sourceName }) => sourceName)
+
+    const columns: Array<ColumnProps<IViewBase>> = [{
+      title: '名称',
+      dataIndex: 'name',
+      filterDropdown: (
+        <SearchFilterDropdown
+          placeholder='名称'
+          value={tempFilterViewName}
+          onChange={this.filterViewNameChange}
+          onSearch={this.searchView}
+        />
+      ),
+      filterDropdownVisible,
+      onFilterDropdownVisibleChange: (visible: boolean) => this.setState({ filterDropdownVisible: visible }),
+      sorter: (a, b) => (a.name > b.name ? 1 : -1),
+      sortOrder: tableSorter && tableSorter.columnKey === 'name' ? tableSorter.order : void 0
+    }, {
+      title: '描述',
+      dataIndex: 'description'
+    }, {
+      title: '数据源',
+      // title: 'Source',
+      dataIndex: 'sourceName',
+      filterMultiple: false,
+      onFilter: (val, record) => record.sourceName === val,
+      filters: sourceNames
+        .filter((name, idx) => sourceNames.indexOf(name) === idx)
+        .map((name) => ({ text: name, value: name }))
+    }]
+
+    if (filterViewName) {
+      const regex = new RegExp(`(${filterViewName})`, 'gi')
+      columns[0].render = (text: string) => (
+        <span
+          dangerouslySetInnerHTML={{
+            __html: text.replace(regex, `<span class='${utilStyles.highlight}'>$1</span>`)
+          }}
+        />
+      )
+    }
+
+    if (viewPermission) {
+      columns.push({
+        title: '操作',
+        width: 150,
+        className: utilStyles.textAlignCenter,
+        render: (_, record) => (
+          <span className='ant-table-action-column'>
+            <Tooltip title='复制'>
+              <EditButton icon='copy' shape='circle' type='ghost' onClick={this.copyView(record)} />
+            </Tooltip>
+            <Tooltip title='修改'>
+              <EditButton icon='edit' shape='circle' type='ghost' onClick={this.editView(record.id)} />
+            </Tooltip>
+            <Popconfirm
+              title='确定删除?'
+              placement='bottom'
+              onConfirm={this.deleteView(record.id)}
+            >
+              <Tooltip title='删除'>
+                <AdminButton icon='delete' shape='circle' type='ghost' />
+              </Tooltip>
+            </Popconfirm>
+          </span>
+        )
+      })
+    }
+
+    return columns
+  }
+
+  private tableChange = (_1, _2, sorter: SorterResult<IViewBase>) => {
+    this.setState({ tableSorter: sorter })
+  }
+
+  private filterViewNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    this.setState({
+      tempFilterViewName: e.target.value,
+      filterViewName: ''
+    })
+  }
+
+  private searchView = (value: string) => {
+    this.setState({
+      filterViewName: value,
+      filterDropdownVisible: false
+    })
+    window.event.preventDefault()
+  }
+
+  private basePagination: PaginationConfig = {
+    defaultPageSize: 20,
+    showSizeChanger: true
+  }
+
+  private addView = () => {
+    const { history, match } = this.props
+    history.push(`/project/${match.params.projectId}/view`)
+  }
+
+  private copyView = (fromView: IViewBase) => () => {
+    this.setState({
+      copyModalVisible: true,
+      copyFromView: fromView
+    })
+  }
+
+  private copy = (view: IViewBase) => {
+    const { onCopyView } = this.props
+    onCopyView(view, () => {
+      this.setState({
+        copyModalVisible: false
+      })
+      message.info('View 复制成功')
+    })
+  }
+
+  private cancelCopy = () => {
+    this.setState({ copyModalVisible: false })
+  }
+
+  private editView = (viewId: number) => () => {
+    const { history, match } = this.props
+    history.push(`/project/${match.params.projectId}/view/${viewId}`)
+  }
+
+  private deleteView = (viewId: number) => () => {
+    const { onDeleteView } = this.props
+    onDeleteView(viewId, () => {
+      this.loadViews()
+    })
+  }
+
+  private checkViewUniqueName = (viewName: string, resolve: () => void, reject: (err: string) => void) => {
+    const { currentProject, onCheckName } = this.props
+    onCheckName({ name: viewName, projectId: currentProject.id }, resolve, reject)
+  }
+
+  private getCatalogues = async() => {
+    try {
+      const { projectId } = this.props.match.params
+
+      this.setState({ treeLoading: true })
+      // @ts-ignore
+      const { payload } = await request(api.getCatalogues + `?projectId=${projectId}`, { method: 'GET' })
+      this.setState({
+        catalogues: (payload as unknown as ICatalogue[]),
+        selectedCatalogueKeys: payload?.[0]?.id ? [`${payload?.[0]?.id}`] : []
+      })
+    } catch (e) {
+      console.log()
+    } finally {
+      this.setState({ treeLoading: false })
+    }
+  }
+
+  private handleSaveCatalogue = async(catalogue: ICatalogue) => {
+    const { projectId } = this.props.match.params
+    if (!projectId) {
+      return
+    }
+    try {
+      const parentId = Number(this.state.selectedCatalogueKeys[0]) || null
+      const catalogueFromView = this.state.catalogueFromView
+      this.setState({ saveCatalogueLoading: true })
+      // const api = this.state.catalogueFromView ? api.updateCatalogue : api.createCatalogue
+      if (catalogueFromView) {
+        await request(api.updateCatalogue + `/${catalogueFromView.id}`, { method: 'PUT' })
+      } else {
+        await request(api.createCatalogue, { method: 'post', data: { ...catalogue, parentId, projectId } })
+      }
+      this.setState({ saveCatalogueLoading: true, catalogueModalVisible: false })
+      this.getCatalogues()
+    } finally {
+      this.setState({
+        saveCatalogueLoading: false,
+        catalogueFromView: null
+      })
+    }
+  }
+
+  private renderTree = (catalogues: ICatalogue[]) => {
+    const { selectedCatalogueKeys } = this.state
+    // tslint:disable-next-line:jsx-wrap-multiline
+    return <>
+      {
+        catalogues.map((c, idx) => (
+          <>
+            <div
+              key={c.id ?? idx}
+              className={classnames(styles.treeNode, {
+                [styles.treeNodeSelected]: selectedCatalogueKeys.includes(`${c.id}`),
+                [styles.treeNodeChild]: !!c.parentId
+              })}
+            >
+          <span
+            className={styles.treeNodeLeft}
+            onClick={() => {
+              this.setState({ selectedCatalogueKeys: [`${c.id}`] }, () => {
+                console.log(this.state.selectedCatalogueKeys, '~~~')
+                this.loadViews()
+              })
+            }}
+          >
+            <Icon type='folder-open' />
+            {c.name}</span>
+              <span>
+             <Dropdown
+               overlay={() => (
+                 <Menu>
+                   <Menu.Item
+                     key='0'
+                     onClick={() => {
+                       this.setState({
+                         catalogueModalVisible: true,
+                         catalogueFromView: c
+                       })
+                     }}
+                   >编辑</Menu.Item>
+                   <Menu.Item
+                     key='1'
+                     onClick={() => {
+                       Modal.confirm({
+                         title: '确认删除吗?',
+                         onOk: async() => {
+                           try {
+                             const data = await request(api.deleteCatalogue + `/${c.id}`, { method: 'DELETE' })
+                             console.log(data)
+
+                             if (data?.header?.code === 200) {
+                               this.getCatalogues()
+                             }
+                           } catch (e) {
+
+                           }
+                         }
+                       })
+                     }}
+                   >
+                     删除
+                   </Menu.Item>
+                 </Menu>
+               )}
+               trigger={['click']}
+             >
+                <Icon type='more' />
+            </Dropdown>
+           </span>
+            </div>
+            <div style={{ marginLeft: 20 }}>
+              {c.children && this.renderTree(c.children)}
+            </div>
+          </>
+        ))
+      }
+    </>
+  }
+
+  public render() {
+    const { currentProject, views, loading } = this.props
+    const { screenWidth, filterViewName, viewList } = this.state
+    const { viewPermission, AdminButton, EditButton } = ViewList.getViewPermission(currentProject)
+    const tableColumns = this.getTableColumns({ viewPermission, AdminButton, EditButton })
+    const tablePagination: PaginationConfig = {
+      ...this.basePagination,
+      simple: screenWidth <= 768
+    }
+    const filterViews = this.getFilterViews(filterViewName, viewList)
+
+    const { copyModalVisible, copyFromView, catalogueModalVisible, catalogueFromView } = this.state
+
+    const pathname = this.props.history.location.pathname
+
+    return (
+      <>
+        <Container>
+          <Helmet title='数据资产' />
+          {
+            !pathname.includes('dataManager') && <ContainerTitle>
+              <Row>
+                <Col span={24} className={utilStyles.shortcut}>
+                  <Breadcrumb className={utilStyles.breadcrumb}>
+                    <Breadcrumb.Item>
+                      <Link to=''>View</Link>
+                    </Breadcrumb.Item>
+                  </Breadcrumb>
+                  <Link to={`/account/organization/${currentProject.orgId}`}>
+                    <i className='iconfont icon-organization' />
+                  </Link>
+                </Col>
+              </Row>
+            </ContainerTitle>
+          }
+          <ContainerBody>
+            <Box>
+              <Box.Header>
+                <Box.Title>
+                  <Icon type='bars' />
+                  数据资产列表
+                </Box.Title>
+                <Box.Tools>
+                  <Tooltip placement='bottom' title='新增'>
+                    <AdminButton type='primary' icon='plus' onClick={this.addView} />
+                  </Tooltip>
+                </Box.Tools>
+              </Box.Header>
+              <Box.Body>
+                <div className={styles.treeTableContainer}>
+                  <div className={styles.treeContainer}>
+                    <div className={styles.treeTitle}>
+                      <h6>资源目录列表</h6>
+                      <div
+                        className={styles.treePlusNode}
+                        onClick={
+                          () => {
+                            this.setState({ catalogueModalVisible: true })
+                          }
+                        }
+                      >
+                        <Icon type='plus' />
+                      </div>
+                    </div>
+                    <div className={styles.treeContent}>
+                      <Spin spinning={this.state.treeLoading}>
+                        {this.renderTree(this.state.catalogues)}
+                      </Spin>
+                    </div>
+                  </div>
+                  <Table
+                    style={{ flex: 1 }}
+                    bordered
+                    rowKey='id'
+                    loading={this.state.tableLoading}
+                    dataSource={filterViews}
+                    columns={tableColumns}
+                    pagination={tablePagination}
+                    onChange={this.tableChange}
+                  />
+                </div>
+              </Box.Body>
+            </Box>
+          </ContainerBody>
+        </Container>
+        <CopyModal
+          visible={copyModalVisible}
+          loading={loading.copy}
+          fromView={copyFromView}
+          onCheckUniqueName={this.checkViewUniqueName}
+          onCopy={this.copy}
+          onCancel={this.cancelCopy}
+        />
+        <CatalogueModal
+          visible={catalogueModalVisible}
+          loading={this.state.saveCatalogueLoading}
+          fromView={catalogueFromView}
+          onCheckUniqueName={this.checkViewUniqueName}
+          onSave={this.handleSaveCatalogue}
+          onCancel={() => this.setState({ catalogueModalVisible: false })}
+        />
+      </>
+    )
+  }
+
+}
+
+const mapDispatchToProps = (dispatch: Dispatch<ViewActionType>) => ({
+  onLoadViews: (projectId, parentId) => dispatch(ViewActions.loadViews(projectId, parentId)),
+  onDeleteView: (viewId, resolve) => dispatch(ViewActions.deleteView(viewId, resolve)),
+  onCopyView: (view, resolve) => dispatch(ViewActions.copyView(view, resolve)),
+  onCheckName: (data, resolve, reject) => dispatch(checkNameUniqueAction('view', data, resolve, reject))
+})
+
+const mapStateToProps = createStructuredSelector({
+  views: makeSelectViews(),
+  currentProject: makeSelectCurrentProject(),
+  loading: makeSelectLoading()
+})
+
+const withConnect = connect<IViewListStateProps, IViewListDispatchProps, RouteComponentWithParams>(mapStateToProps, mapDispatchToProps)
+const withReducer = injectReducer({ key: 'view', reducer })
+const withSaga = injectSaga({ key: 'view', saga: sagas })
+
+export default compose(
+  withReducer,
+  withSaga,
+  withConnect
+)(ViewList)

+ 329 - 0
app/containers/DataManagerView/reducer.ts

@@ -0,0 +1,329 @@
+/*
+ * <<
+ * 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 produce from 'immer'
+import pick from 'lodash/pick'
+import { IViewState, IView, IFormedViews, IViewBase } from './types'
+import { getFormedView, getValidModel } from './util'
+
+import { ActionTypes, DEFAULT_SQL_LIMIT } from './constants'
+import { ViewActionType } from './actions'
+
+import { ActionTypes as SourceActionTypes } from 'containers/Source/constants'
+import { SourceActionType } from 'containers/Source/actions'
+
+import { ActionTypes as WidgetActionTypes } from 'containers/Widget/constants'
+import { WidgetActionType } from 'containers/Widget/actions'
+import { ActionTypes as DashboardActionTypes } from 'containers/Dashboard/constants'
+import { DashboardActionType } from 'containers/Dashboard/actions'
+
+import { ActionTypes as DisplayActionTypes } from 'containers/Display/constants'
+import { DisplayActionType } from 'containers/Display/actions'
+import { LOCATION_CHANGE, LocationChangeAction } from 'connected-react-router'
+
+const emptyView: IView = {
+  id: null,
+  name: '',
+  sql: '',
+  model: '',
+  variable: '',
+  roles: [],
+  config: '',
+  description: '',
+  projectId: null,
+  sourceId: null
+}
+
+const initialState: IViewState = {
+  views: [],
+  formedViews: {},
+  editingView: emptyView,
+  editingViewInfo: {
+    model: {},
+    variable: [],
+    roles: []
+  },
+  sources: [],
+  schema: {
+    mapDatabases: {},
+    mapTables: {},
+    mapColumns: {}
+  },
+  sqlValidation: {
+    code: null,
+    message: null
+  },
+  sqlDataSource: {
+    columns: [],
+    totalCount: 0,
+    resultList: []
+  },
+  sqlLimit: DEFAULT_SQL_LIMIT,
+  loading: {
+    view: false,
+    table: false,
+    modal: false,
+    execute: false,
+    copy: false
+  },
+
+  channels: [],
+  tenants: [],
+  bizs: [],
+  cancelTokenSources: [],
+  isLastExecuteWholeSql: true
+}
+
+const viewReducer = (
+  state = initialState,
+  action:
+    | ViewActionType
+    | WidgetActionType
+    | DashboardActionType
+    | DisplayActionType
+    | SourceActionType
+    | LocationChangeAction
+): IViewState =>
+  produce(state, (draft) => {
+    switch (action.type) {
+      case ActionTypes.LOAD_VIEWS:
+      case ActionTypes.DELETE_VIEW:
+        draft.loading.view = true
+        break
+      case ActionTypes.LOAD_VIEWS_FAILURE:
+      case ActionTypes.DELETE_VIEW_FAILURE:
+        draft.loading.view = false
+        break
+      case ActionTypes.LOAD_VIEWS_SUCCESS:
+        draft.views = action.payload.views
+        draft.formedViews = Object.entries(draft.formedViews).reduce(
+          (obj, [viewId, formedView]) => {
+            const existView = action.payload.views.find(
+              (v) => v.id === Number(viewId)
+            )
+            if (existView) {
+              obj[viewId] = formedView
+            }
+            return obj
+          },
+          {}
+        )
+        draft.loading.view = false
+        break
+      case ActionTypes.LOAD_VIEWS_DETAIL:
+        draft.formedViews = action.payload.viewIds.reduce((acc, id) => {
+          if (!acc[id]) {
+            acc[id] = {
+              id,
+              name: '',
+              description: '',
+              sql: '',
+              config: '',
+              sourceId: 0,
+              projectId: 0,
+              model: {},
+              variable: [],
+              roles: []
+            }
+          }
+          return acc
+        }, draft.formedViews)
+        break
+      case ActionTypes.LOAD_VIEWS_DETAIL_SUCCESS:
+        const detailedViews = action.payload.views
+        if (action.payload.isEditing) {
+          draft.editingView = detailedViews[0]
+          draft.editingViewInfo = pick(getFormedView(detailedViews[0]), [
+            'model',
+            'variable',
+            'roles'
+          ])
+        }
+        draft.formedViews = detailedViews.reduce((acc, view) => {
+          const { id, model, variable, roles } = getFormedView(view)
+          acc[id] = {
+            ...view,
+            model,
+            variable,
+            roles
+          }
+          return acc
+        }, draft.formedViews)
+        break
+      case SourceActionTypes.LOAD_SOURCES_SUCCESS:
+        draft.sources = action.payload.sources
+        draft.schema = {
+          mapDatabases: {},
+          mapTables: {},
+          mapColumns: {}
+        }
+        break
+      case SourceActionTypes.LOAD_SOURCE_DATABASES_SUCCESS:
+        const { sourceDatabases } = action.payload
+        draft.schema.mapDatabases[sourceDatabases.sourceId] =
+          sourceDatabases.databases
+        break
+      case SourceActionTypes.LOAD_SOURCE_DATABASE_TABLES_SUCCESS:
+        const { databaseTables } = action.payload
+        draft.schema.mapTables[
+          `${databaseTables.sourceId}_${databaseTables.dbName}`
+        ] = databaseTables
+        break
+      case SourceActionTypes.LOAD_SOURCE_TABLE_COLUMNS_SUCCESS:
+        const { databaseName, tableColumns } = action.payload
+        draft.schema.mapColumns[
+          `${tableColumns.sourceId}_${databaseName}_${tableColumns.tableName}`
+        ] = tableColumns
+        break
+      case ActionTypes.IS_LAST_EXECUTE_WHOLE_SQL:
+        draft.isLastExecuteWholeSql = action.payload.isLastExecuteWholeSql
+        break
+      case ActionTypes.EXECUTE_SQL:
+        draft.loading.execute = true
+        draft.sqlValidation = { code: null, message: null }
+        break
+      case ActionTypes.EXECUTE_SQL_SUCCESS:
+        const sqlResponse = action.payload.result
+        const validModel = getValidModel(
+          draft.editingViewInfo.model,
+          sqlResponse.payload.columns
+        )
+        draft.sqlDataSource = sqlResponse.payload
+        draft.editingViewInfo.model = validModel
+        draft.loading.execute = false
+        draft.sqlValidation = {
+          code: sqlResponse.header.code,
+          message: sqlResponse.header.msg
+        }
+        break
+      case ActionTypes.EXECUTE_SQL_FAILURE:
+        draft.sqlDataSource = {
+          ...draft.sqlDataSource,
+          columns: [],
+          totalCount: 0,
+          resultList: []
+        }
+        draft.loading.execute = false
+        draft.sqlValidation = {
+          code: action.payload.err.code,
+          message: action.payload.err.msg
+        }
+        break
+      case ActionTypes.EXECUTE_SQL_CANCEL:
+        draft.sqlDataSource = {
+          ...draft.sqlDataSource,
+          columns: [],
+          totalCount: 0,
+          resultList: []
+        }
+        draft.loading.execute = false
+        break
+      case ActionTypes.UPDATE_EDITING_VIEW:
+        draft.editingView = action.payload.view
+        break
+      case ActionTypes.UPDATE_EDITING_VIEW_INFO:
+        draft.editingViewInfo = action.payload.viewInfo
+        break
+      case ActionTypes.SET_SQL_LIMIT:
+        draft.sqlLimit = action.payload.limit
+        break
+      case ActionTypes.EDIT_VIEW_SUCCESS:
+        draft.editingView = emptyView
+        draft.editingViewInfo = { model: {}, variable: [], roles: [] }
+        draft.formedViews[action.payload.result.id] = getFormedView(
+          action.payload.result
+        )
+        break
+
+      case ActionTypes.COPY_VIEW:
+        draft.loading.copy = true
+        break
+      case ActionTypes.COPY_VIEW_SUCCESS:
+        const fromViewId = action.payload.fromViewId
+        const { id, name, description, source } = action.payload.result
+        const copiedView: IViewBase = {
+          id,
+          name,
+          description,
+          sourceName: source.name
+        }
+        draft.views.splice(
+          draft.views.findIndex(({ id }) => id === fromViewId) + 1,
+          0,
+          copiedView
+        )
+        draft.loading.copy = false
+        break
+      case ActionTypes.COPY_VIEW_FAILURE:
+        draft.loading.copy = false
+        break
+
+      case ActionTypes.LOAD_DAC_CHANNELS_SUCCESS:
+        draft.channels = action.payload.channels
+        break
+      case ActionTypes.LOAD_DAC_TENANTS_SUCCESS:
+        draft.tenants = action.payload.tenants
+        break
+      case ActionTypes.LOAD_DAC_TENANTS_FAILURE:
+        draft.tenants = []
+        break
+      case ActionTypes.LOAD_DAC_BIZS_SUCCESS:
+        draft.bizs = action.payload.bizs
+        break
+      case ActionTypes.LOAD_DAC_BIZS_FAILURE:
+        draft.bizs = []
+        break
+      case ActionTypes.RESET_VIEW_STATE:
+        return initialState
+        break
+      case WidgetActionTypes.LOAD_WIDGET_DETAIL_SUCCESS:
+        const widgetView = action.payload.view
+        draft.formedViews[widgetView.id] = {
+          ...widgetView,
+          model: JSON.parse(widgetView.model || '{}'),
+          variable: JSON.parse(widgetView.variable || '[]')
+        }
+        break
+      case DashboardActionTypes.LOAD_DASHBOARD_DETAIL_SUCCESS:
+      case DisplayActionTypes.LOAD_SLIDE_DETAIL_SUCCESS:
+        draft.formedViews = {
+          ...draft.formedViews,
+          ...action.payload.formedViews
+        }
+        break
+      case ActionTypes.LOAD_VIEW_DATA_FROM_VIZ_ITEM:
+      case ActionTypes.LOAD_SELECT_OPTIONS:
+        draft.cancelTokenSources.push(action.payload.cancelTokenSource)
+        break
+      case LOCATION_CHANGE:
+        if (state.cancelTokenSources.length) {
+          state.cancelTokenSources.forEach((source) => {
+            source.cancel()
+          })
+          draft.cancelTokenSources = []
+        }
+        break
+      default:
+        break
+    }
+  })
+
+export { initialState as viewInitialState }
+export default viewReducer

+ 377 - 0
app/containers/DataManagerView/sagas.ts

@@ -0,0 +1,377 @@
+/*
+ * <<
+ * 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 { call, put, all, takeLatest, takeEvery } from 'redux-saga/effects'
+import { ActionTypes } from './constants'
+import { ViewActions, ViewActionType } from './actions'
+import omit from 'lodash/omit'
+
+import axios, { AxiosResponse, AxiosError, CancelTokenSource } from 'axios'
+import request, { IDavinciResponse } from 'utils/request'
+import api from 'utils/api'
+import { errorHandler, getErrorMessage } from 'utils/util'
+
+import { IViewBase, IView, IExecuteSqlResponse, IViewVariable } from './types'
+import { EExecuteType } from './Editor'
+
+export function* getViews (action: ViewActionType) {
+  if (action.type !== ActionTypes.LOAD_VIEWS) { return }
+  const { payload } = action
+  const { viewsLoaded, loadViewsFail } = ViewActions
+  let views: IViewBase[]
+  try {
+    const asyncData = yield call(request, `${api.view}?projectId=${payload.projectId}&parentId=${payload.parentId}`)
+    views = asyncData.payload
+    yield put(viewsLoaded(views))
+  } catch (err) {
+    yield put(loadViewsFail())
+    errorHandler(err)
+  } finally {
+    if (payload.resolve) {
+      payload.resolve(views)
+    }
+  }
+}
+
+export function* getViewsDetail (action: ViewActionType) {
+  if (action.type !== ActionTypes.LOAD_VIEWS_DETAIL) { return }
+  const { payload } = action
+  const { viewsDetailLoaded, loadViewsDetailFail } = ViewActions
+  const { viewIds, resolve, isEditing } = payload
+  try {
+    // @FIXME make it be a single request
+    const asyncData = yield all(viewIds.map((viewId) => (call(request, `${api.view}/${viewId}`))))
+    const views: IView[] = asyncData.map((item) => item.payload)
+    yield put(viewsDetailLoaded(views, isEditing))
+    if (resolve) { resolve(views) }
+  } catch (err) {
+    yield put(loadViewsDetailFail())
+    errorHandler(err)
+  }
+}
+
+export function* addView (action: ViewActionType) {
+  if (action.type !== ActionTypes.ADD_VIEW) { return }
+  const { payload } = action
+  const { view, resolve } = payload
+  const { viewAdded, addViewFail } = ViewActions
+  try {
+    const asyncData = yield call(request, {
+      method: 'post',
+      url: api.view,
+      data: view
+    })
+    yield put(viewAdded(asyncData.payload))
+    resolve()
+  } catch (err) {
+    yield put(addViewFail())
+    errorHandler(err)
+  }
+}
+
+export function* editView (action: ViewActionType) {
+  if (action.type !== ActionTypes.EDIT_VIEW) { return }
+  const { payload } = action
+  const { view, resolve } = payload
+  const { viewEdited, editViewFail } = ViewActions
+  try {
+    yield call(request, {
+      method: 'put',
+      url: `${api.view}/${view.id}`,
+      data: view
+    })
+    yield put(viewEdited(view))
+    resolve()
+  } catch (err) {
+    yield put(editViewFail())
+    errorHandler(err)
+  }
+}
+
+export function* deleteView (action: ViewActionType) {
+  if (action.type !== ActionTypes.DELETE_VIEW) { return }
+  const { payload } = action
+  const { viewDeleted, deleteViewFail } = ViewActions
+  try {
+    yield call(request, {
+      method: 'delete',
+      url: `${api.view}/${payload.id}`
+    })
+    yield put(viewDeleted(payload.id))
+    payload.resolve(payload.id)
+  } catch (err) {
+    yield put(deleteViewFail())
+    errorHandler(err)
+  }
+}
+
+export function* copyView (action: ViewActionType) {
+  if (action.type !== ActionTypes.COPY_VIEW) { return }
+  const { view, resolve } = action.payload
+  const { viewCopied, copyViewFail } = ViewActions
+  try {
+    const fromViewResponse = yield call(request, `${api.view}/${view.id}`)
+    const fromView = fromViewResponse.payload
+    const copyView: IView = { ...fromView, name: view.name, description: view.description }
+    const asyncData = yield call(request, {
+      method: 'post',
+      url: api.view,
+      data: copyView
+    })
+    yield put(viewCopied(fromView.id, asyncData.payload))
+    resolve()
+  } catch (err) {
+    yield put(copyViewFail())
+    errorHandler(err)
+  }
+}
+
+let cancelTokenSource = null as CancelTokenSource
+export function* executeSql (action: ViewActionType) {
+  if (action.type !== ActionTypes.EXECUTE_SQL) { return }
+  const { sqlExecuted, executeSqlFail, executeSqlCancel, setIsLastExecuteWholeSql } = ViewActions
+  if (cancelTokenSource) {
+    cancelTokenSource.cancel('cancel execute')
+    yield put(executeSqlCancel())
+    return cancelTokenSource = null
+  }
+  cancelTokenSource = axios.CancelToken.source()
+  const { params, exeType } = action.payload
+  const { variables, ...rest } = params
+  const omitKeys: Array<keyof IViewVariable> = ['key', 'alias', 'fromService']
+  const variableParam = variables.map((v) => omit(v, omitKeys))
+  try {
+    const asyncData: IDavinciResponse<IExecuteSqlResponse> = yield call(request, {
+      method: 'post',
+      url: `${api.view}/executesql`,
+      data: {
+        ...rest,
+        variables: variableParam
+      },
+      cancelToken: cancelTokenSource.token
+    })
+    yield put(sqlExecuted(asyncData))
+    const isLastExecuteWholeSql = exeType === EExecuteType.whole ? true : false
+    yield put(setIsLastExecuteWholeSql(isLastExecuteWholeSql))
+    cancelTokenSource = null
+  } catch (err) {
+    const { response } = err as AxiosError
+    const { data } = response as AxiosResponse<IDavinciResponse<any>>
+    yield put(executeSqlFail(data.header))
+    cancelTokenSource = null
+  }
+}
+
+/** View sagas for external usages */
+export function* getViewData (action: ViewActionType) {
+  if (action.type !== ActionTypes.LOAD_VIEW_DATA) { return }
+  const { id, requestParams, resolve, reject } = action.payload
+  const { viewDataLoaded, loadViewDataFail } = ViewActions
+  try {
+    const asyncData = yield call(request, {
+      method: 'post',
+      url: `${api.view}/${id}/getdata`,
+      data: requestParams
+    })
+    yield put(viewDataLoaded())
+    asyncData.payload.resultList = asyncData.payload.resultList || []
+    resolve(asyncData.payload)
+  } catch (err) {
+    const { response } = err as AxiosError
+    const { data } = response as AxiosResponse<IDavinciResponse<any>>
+    yield put(loadViewDataFail(err))
+    reject(data.header)
+  }
+}
+
+export function* getSelectOptions (action: ViewActionType) {
+  if (action.type !== ActionTypes.LOAD_SELECT_OPTIONS) {
+    return
+  }
+  const { selectOptionsLoaded, loadSelectOptionsFail } = ViewActions
+  try {
+    const {
+      controlKey,
+      requestParams,
+      itemId,
+      cancelTokenSource
+    } = action.payload
+    const requests = Object.entries(requestParams).map(([viewId, params]) => {
+      const { columns, filters, variables, cache, expired } = params
+      return call(request, {
+        method: 'post',
+        url: `${api.view}/${viewId}/getdistinctvalue`,
+        data: {
+          columns,
+          filters,
+          params: variables,
+          cache,
+          expired
+        },
+        cancelToken: cancelTokenSource.token
+      })
+    })
+    const results: Array<IDavinciResponse<object[]>> = yield all(requests)
+    yield put(selectOptionsLoaded(
+      controlKey,
+      results.reduce((arr, result) => arr.concat(result.payload), []),
+      itemId
+    ))
+  } catch (err) {
+    yield put(loadSelectOptionsFail(err))
+  }
+}
+
+export function* getColumnDistinctValue(action: ViewActionType) {
+  if (action.type !== ActionTypes.LOAD_COLUMN_DISTINCT_VALUE) {
+    return
+  }
+  const { paramsByViewId, callback } = action.payload
+
+  try {
+    const requests = Object.entries(paramsByViewId).map(([viewId, params]) => {
+      return call(request, {
+        method: 'post',
+        url: `${api.view}/${viewId}/getdistinctvalue`,
+        data: {
+          ...params,
+          cache: false,
+          expired: 0,
+          columns: params.columns
+        }
+      })
+    })
+    const results: Array<IDavinciResponse<object[]>> = yield all(requests)
+    callback(results.reduce((arr, result) => arr.concat(result.payload), []))
+  } catch (err) {
+    callback()
+    errorHandler(err)
+  }
+}
+
+export function* getViewDataFromVizItem (action: ViewActionType) {
+  if (action.type !== ActionTypes.LOAD_VIEW_DATA_FROM_VIZ_ITEM) { return }
+  const { renderType, itemId, viewId, requestParams, vizType, cancelTokenSource } = action.payload
+  const { viewDataFromVizItemLoaded, loadViewDataFromVizItemFail } = ViewActions
+  const {
+    filters,
+    tempFilters,  // @TODO combine widget static filters with local filters
+    linkageFilters,
+    globalFilters,
+    variables,
+    linkageVariables,
+    globalVariables,
+    pagination,
+    drillStatus,
+    groups,
+    ...rest
+  } = requestParams
+  const { pageSize, pageNo } = pagination || { pageSize: 0, pageNo: 0 }
+
+  let searchFilters = filters.concat(tempFilters).concat(linkageFilters).concat(globalFilters)
+  if (drillStatus && drillStatus.filters) {
+    searchFilters = searchFilters.concat(drillStatus.filters)  // 改成 drillStatus.filters
+  }
+
+  try {
+    const asyncData = yield call(request, {
+      method: 'post',
+      url: `${api.view}/${viewId}/getdata`,
+      data: {
+        ...omit(rest, 'customOrders'),
+        groups:  drillStatus && drillStatus.groups ? drillStatus.groups : groups,
+        filters: searchFilters,
+        params: variables.concat(linkageVariables).concat(globalVariables),
+        pageSize,
+        pageNo
+      },
+      cancelToken: cancelTokenSource.token
+    })
+    asyncData.payload = asyncData.payload || {}
+    const { payload } = asyncData
+    payload.resultList = payload.resultList || []
+    yield put(viewDataFromVizItemLoaded(renderType, itemId, requestParams, asyncData.payload, vizType, action.statistic))
+  } catch (err) {
+    yield put(loadViewDataFromVizItemFail(itemId, vizType, getErrorMessage(err)))
+  }
+}
+/** */
+
+/** View sagas for fetch external authorization variables values */
+export function* getDacChannels (action: ViewActionType) {
+  if (action.type !== ActionTypes.LOAD_DAC_CHANNELS) { return }
+  const { dacChannelsLoaded, loadDacChannelsFail } = ViewActions
+  try {
+    const asyncData = yield call(request, `${api.view}/dac/channels`)
+    const channels = asyncData.payload
+    yield put(dacChannelsLoaded(channels))
+  } catch (err) {
+    yield put(loadDacChannelsFail())
+    errorHandler(err)
+  }
+}
+export function* getDacTenants (action: ViewActionType) {
+  if (action.type !== ActionTypes.LOAD_DAC_TENANTS) { return }
+  const { dacTenantsLoaded, loadDacTenantsFail } = ViewActions
+  const { channelName } = action.payload
+  try {
+    const asyncData = yield call(request, `${api.view}/dac/${channelName}/tenants`)
+    const tenants = asyncData.payload
+    yield put(dacTenantsLoaded(tenants))
+  } catch (err) {
+    yield put(loadDacTenantsFail())
+    errorHandler(err)
+  }
+}
+export function* getDacBizs (action: ViewActionType) {
+  if (action.type !== ActionTypes.LOAD_DAC_BIZS) { return }
+  const { dacBizsLoaded, loadDacBizsFail } = ViewActions
+  const { channelName, tenantId } = action.payload
+  try {
+    const asyncData = yield call(request, `${api.view}/dac/${channelName}/tenants/${tenantId}/bizs`)
+    const bizs = asyncData.payload
+    yield put(dacBizsLoaded(bizs))
+  } catch (err) {
+    yield put(loadDacBizsFail())
+    errorHandler(err)
+  }
+}
+/** */
+
+export default function* rootViewSaga () {
+  yield all([
+    takeLatest(ActionTypes.LOAD_VIEWS, getViews),
+    takeEvery(ActionTypes.LOAD_VIEWS_DETAIL, getViewsDetail),
+    takeLatest(ActionTypes.ADD_VIEW, addView),
+    takeEvery(ActionTypes.EDIT_VIEW, editView),
+    takeEvery(ActionTypes.DELETE_VIEW, deleteView),
+    takeEvery(ActionTypes.COPY_VIEW, copyView),
+    takeLatest(ActionTypes.EXECUTE_SQL, executeSql),
+
+    takeEvery(ActionTypes.LOAD_VIEW_DATA, getViewData),
+    takeEvery(ActionTypes.LOAD_SELECT_OPTIONS, getSelectOptions),
+    takeEvery(ActionTypes.LOAD_COLUMN_DISTINCT_VALUE, getColumnDistinctValue),
+    takeEvery(ActionTypes.LOAD_VIEW_DATA_FROM_VIZ_ITEM, getViewDataFromVizItem),
+
+    takeEvery(ActionTypes.LOAD_DAC_CHANNELS, getDacChannels),
+    takeEvery(ActionTypes.LOAD_DAC_TENANTS, getDacTenants),
+    takeEvery(ActionTypes.LOAD_DAC_BIZS, getDacBizs)
+  ])
+}

+ 112 - 0
app/containers/DataManagerView/selectors.ts

@@ -0,0 +1,112 @@
+/*
+ * <<
+ * 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 { createSelector } from 'reselect'
+import { IViewState } from './types'
+
+const selectView = (state) => state.view
+
+const makeSelectViews = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.views
+)
+
+const makeSelectEditingView = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.editingView
+)
+
+const makeSelectEditingViewInfo = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.editingViewInfo
+)
+
+const makeSelectFormedViews = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.formedViews
+)
+
+const makeSelectSources = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.sources
+)
+
+const makeSelectSchema = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.schema
+)
+
+const makeSelectSqlValidation = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.sqlValidation
+)
+
+const makeSelectSqlDataSource = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.sqlDataSource
+)
+
+const makeSelectSqlLimit = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.sqlLimit
+)
+
+const makeSelectLoading = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.loading
+)
+
+const makeSelectChannels = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.channels
+)
+const makeSelectTenants = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.tenants
+)
+const makeSelectBizs = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.bizs
+)
+
+const makeSelectIsLastExecuteWholeSql = () => createSelector(
+  selectView,
+  (viewState: IViewState) => viewState.isLastExecuteWholeSql
+)
+
+export {
+  selectView,
+  makeSelectViews,
+  makeSelectEditingView,
+  makeSelectEditingViewInfo,
+  makeSelectFormedViews,
+  makeSelectSources,
+  makeSelectSchema,
+  makeSelectSqlValidation,
+  makeSelectSqlDataSource,
+  makeSelectSqlLimit,
+  makeSelectLoading,
+
+  makeSelectChannels,
+  makeSelectTenants,
+  makeSelectBizs,
+
+  makeSelectIsLastExecuteWholeSql
+}

+ 219 - 0
app/containers/DataManagerView/types.ts

@@ -0,0 +1,219 @@
+/*
+ * <<
+ * 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 { SqlTypes } from 'app/globalConstants'
+import { ISourceSimple, ISourceBase, ISchema } from 'containers/Source/types'
+import {
+  ViewModelTypes,
+  ViewModelVisualTypes,
+  ViewVariableTypes,
+  ViewVariableValueTypes
+} from './constants'
+import { CancelTokenSource } from 'axios'
+
+export interface ICatalogue {
+  description: string // 资源描述
+  extConfig: string // 扩展信息
+  id: number
+  industry: string // 行业分类
+  name: string // 资源名称
+  originDept: string // 来源部门
+  originSystem: string // 来源系统
+  parentId: string
+  projectId: number
+  children?: ICatalogue[]
+}
+
+export interface IViewBase {
+  id: number
+  name: string
+  description: string
+  sourceName: string
+}
+
+type IViewTemp = Omit<IViewBase, 'sourceName'>
+
+export interface IView extends IViewTemp {
+  sql: string
+  model: string
+  variable: string
+  config: string
+  projectId: number
+  source?: ISourceSimple
+  sourceId: number
+  roles: IViewRoleRaw[]
+}
+
+type IViewTemp2 = Omit<Omit<Omit<IView, 'model'>, 'variable'>, 'roles'>
+
+export interface IFormedView extends IViewTemp2 {
+  model: IViewModel
+  variable: IViewVariable[]
+  roles: IViewRole[]
+}
+
+export interface ISqlValidation {
+  code: number
+  message: string
+}
+
+export interface IViewLoading {
+  view: boolean
+  table: boolean
+  modal: boolean
+  execute: boolean
+  copy: boolean
+}
+
+export interface IExecuteSqlParams {
+  sourceId: number
+  sql: string
+  limit: number
+  variables: IViewVariableBase[]
+}
+
+export interface ISqlColumn {
+  name: string
+  type: SqlTypes
+}
+
+export interface IExecuteSqlResponse {
+  columns: ISqlColumn[]
+  totalCount: number
+  resultList: Array<{ [key: string]: string | number }>
+}
+
+export interface IViewModelProps {
+  name: string
+  sqlType: SqlTypes
+  visualType: ViewModelVisualTypes
+  modelType: ViewModelTypes
+}
+
+export type IKeyOfViewModelProps = keyof Omit<IViewModelProps, 'name'>
+
+export interface IViewModel {
+  [name: string]: Omit<IViewModelProps, 'name'>
+}
+
+interface IViewVariableChannel {
+  bizId: number
+  name: string
+  tenantId: number
+}
+
+interface IViewVariableBase {
+  name: string
+  type: ViewVariableTypes
+  valueType: ViewVariableValueTypes
+  defaultValues: Array<string | number | boolean>
+  channel?: IViewVariableChannel
+  udf: boolean
+}
+
+export interface IViewVariable extends IViewVariableBase {
+  key: string
+  alias: string
+  fromService: boolean
+}
+
+export interface IViewRoleRaw {
+  roleId: number
+  columnAuth: string
+  rowAuth: string
+}
+
+export interface IViewRoleRowAuth {
+  name: string
+  values: Array<string | number | boolean>
+  enable: boolean
+}
+
+export interface IViewRole {
+  roleId: number
+  /**
+   * view columns name
+   * @type {string[]}
+   * @memberof IViewRole
+   */
+  columnAuth: string[]
+
+  /**
+   * query variable values
+   * @type {(Array<string | number>)}
+   * @memberof IViewRole
+   */
+  rowAuth: IViewRoleRowAuth[]
+}
+
+export interface IViewInfo {
+  model: IViewModel
+  variable: IViewVariable[]
+  roles: IViewRole[]
+}
+
+export interface IFormedViews {
+  [viewId: string]: IFormedView
+}
+
+export interface IShareFormedViews {
+  [viewId: string]: Pick<IFormedView, 'name' | 'model' | 'variable'> & {
+    dataToken: string
+  }
+}
+
+export type IDacChannel = string
+export interface IDacTenant {
+  id: number
+  name: string
+}
+export interface IDacBiz {
+  id: number
+  name: string
+}
+
+export interface IViewQueryResponse {
+  columns: Array<{ name: string; type: SqlTypes }>
+  pageNo: number
+  pageSize: number
+  totalCount: number
+  resultList: any[]
+}
+
+export interface IViewState {
+  views: IViewBase[]
+  formedViews: IFormedViews
+  editingView: IView
+  editingViewInfo: IViewInfo
+  sources: ISourceBase[]
+  schema: ISchema
+  sqlValidation: ISqlValidation
+  sqlDataSource: IExecuteSqlResponse
+  sqlLimit: number
+  loading: IViewLoading
+
+  channels: IDacChannel[]
+  tenants: IDacTenant[]
+  bizs: IDacBiz[]
+
+  cancelTokenSources: CancelTokenSource[]
+
+  isLastExecuteWholeSql: boolean
+}

+ 116 - 0
app/containers/DataManagerView/util.ts

@@ -0,0 +1,116 @@
+import { IViewModel, ISqlColumn, IView, IFormedView, IViewRoleRaw, IViewRole, IKeyOfViewModelProps } from './types'
+
+import { SqlTypes } from 'app/globalConstants'
+import { DefaultModelTypeSqlTypeSetting, VisualTypeSqlTypeSetting, ViewModelVisualTypes, ViewModelTypes } from './constants'
+import { hasProperty } from 'utils/util'
+
+
+export function getFormedView (view: IView): IFormedView {
+  const { model, variable, roles } = view
+  const formedView = {
+    ...view,
+    model: JSON.parse((model || '{}')),
+    variable: JSON.parse((variable || '[]')),
+    roles: (roles as IViewRoleRaw[]).map<IViewRole>(({ roleId, columnAuth, rowAuth }) => ({
+      roleId,
+      columnAuth: JSON.parse(columnAuth || '[]'),
+      rowAuth: JSON.parse(rowAuth || '[]')
+    }))
+  }
+  return formedView
+}
+
+function getMapKeyByValue (value: SqlTypes, map: typeof VisualTypeSqlTypeSetting | typeof DefaultModelTypeSqlTypeSetting) {
+  let result
+  Object.entries(map).some(([key, values]) => {
+    if (values.includes(value)) {
+      result = key
+      return true
+    }
+  })
+  return result
+}
+
+export function getValidModel (model: IViewModel, sqlColumns: ISqlColumn[]) {
+  if (!Array.isArray(sqlColumns)) { return {} }
+
+  const validModel = sqlColumns.reduce<IViewModel>((accModel, column) => {
+    const { name: columnName, type: columnType } = column
+    const modelItem = model[columnName]
+    if (!modelItem) {
+      accModel[columnName] = {
+        sqlType: columnType,
+
+        // model item which columnType not registered with SQL_TYPES in globalConstants.ts
+        // its default visualType is String and modelType is Category
+        visualType: getMapKeyByValue(columnType, VisualTypeSqlTypeSetting) || ViewModelVisualTypes.String,
+        modelType: getMapKeyByValue(columnType, DefaultModelTypeSqlTypeSetting) || ViewModelTypes.Category
+      }
+    } else {
+      accModel[columnName] = { ...modelItem, sqlType: columnType } // update newest sqlType
+      // verify visualType are valid by the sqlType or not
+      // @TODO recover visualType validation after filter visualType select options by columnType in step2
+      // if (SQL_TYPES.includes(columnType)) { // model item which columnType not registered with SQL_TYPES do not need verify
+      //   if (VisualTypeSqlTypeSetting[modelItem.visualType]
+      //       && !VisualTypeSqlTypeSetting[modelItem.visualType].includes(columnType)) {
+      //     accModel[columnName].visualType = getMapKeyByValue(columnType, VisualTypeSqlTypeSetting)
+      //   }
+      // }
+      // @TODO changed visualType need be shown in step2 corresponding model table cell
+    }
+    return accModel
+  }, {})
+
+  return validModel
+}
+
+export function getValidRoleModelNames (model: IViewModel, modelNames: string[]) {
+  if (!Array.isArray(modelNames)) { return [] }
+
+  const validModelNames = modelNames.filter((name) => !!model[name])
+  return validModelNames
+}
+
+
+export function getTypesOfModelCollect (model: IViewModel, type: IKeyOfViewModelProps) {
+  let target = {}
+  if (model) {
+    return target = Object.keys(model).reduce((iteratee, current) => {
+      iteratee[current] = hasProperty(model[current], type)
+      return iteratee
+    }, {})
+  }
+  return target
+}
+
+
+function cacheManager (model: IViewModel, type: IKeyOfViewModelProps) {
+  const cache = {}
+  return function (name: string, callback) {
+    if (cache['prevModel'] === model) {
+      cache[type] = cache[type] || getTypesOfModelCollect(model, type)
+    } else {
+      cache['prevModel'] = model
+      cache[type] = getTypesOfModelCollect(model, type)
+    }
+    return callback(cache)
+  }
+}
+
+export const getTypesOfModelByKeyName = (model: IViewModel, type: IKeyOfViewModelProps) => (name: string) => {
+  return cacheManager(model, type)(name, (cache) => {
+    if (cache && cache[type]) {
+      return hasProperty(cache[type], name)
+    }
+  })
+}
+
+
+export const getListsByViewModelTypes = (model: IViewModel, type: IKeyOfViewModelProps) => (modelType: ViewModelTypes) => {
+  return cacheManager(model, type)(name, (cache) => {
+    const target = cache[type]
+    return Object.keys(target).reduce((iteratee, current) => {
+      return iteratee.concat(target[current] === modelType ? current : [])
+    }, [])
+  })
+}

+ 1 - 1
app/containers/Source/components/SourceConfigModal.tsx

@@ -272,7 +272,7 @@ const SourceConfigModal: React.FC<ISourceConfigModalProps> = (props) => {
 
   return (
     <Modal
-      title={`${!sourceId ? '新增' : '修改'} Source`}
+      title={`${!sourceId ? '新增' : '修改'}`}
       wrapClassName="ant-modal-large"
       maskClosable={false}
       visible={visible}

+ 18 - 17
app/containers/View/index.tsx

@@ -140,7 +140,7 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
       dataIndex: 'name',
       filterDropdown: (
         <SearchFilterDropdown
-          placeholder='名称'
+          placeholder="名称"
           value={tempFilterViewName}
           onChange={this.filterViewNameChange}
           onSearch={this.searchView}
@@ -181,20 +181,20 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
         width: 150,
         className: utilStyles.textAlignCenter,
         render: (_, record) => (
-          <span className='ant-table-action-column'>
-            <Tooltip title='复制'>
-              <EditButton icon='copy' shape='circle' type='ghost' onClick={this.copyView(record)} />
+          <span className="ant-table-action-column">
+            <Tooltip title="复制">
+              <EditButton icon="copy" shape="circle" type="ghost" onClick={this.copyView(record)} />
             </Tooltip>
-            <Tooltip title='修改'>
-              <EditButton icon='edit' shape='circle' type='ghost' onClick={this.editView(record.id)} />
+            <Tooltip title="修改">
+              <EditButton icon="edit" shape="circle" type="ghost" onClick={this.editView(record.id)} />
             </Tooltip>
             <Popconfirm
-              title='确定删除?'
-              placement='bottom'
+              title="确定删除?"
+              placement="bottom"
               onConfirm={this.deleteView(record.id)}
             >
-              <Tooltip title='删除'>
-                <AdminButton icon='delete' shape='circle' type='ghost' />
+              <Tooltip title="删除">
+                <AdminButton icon="delete" shape="circle" type="ghost" />
               </Tooltip>
             </Popconfirm>
           </span>
@@ -290,18 +290,18 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
     return (
       <>
         <Container>
-          <Helmet title='数据资产' />
+          <Helmet title="数据资产" />
           {
             !pathname.includes('dataManager') && <ContainerTitle>
               <Row>
                 <Col span={24} className={utilStyles.shortcut}>
                   <Breadcrumb className={utilStyles.breadcrumb}>
                     <Breadcrumb.Item>
-                      <Link to=''>View</Link>
+                      <Link to="">View</Link>
                     </Breadcrumb.Item>
                   </Breadcrumb>
                   <Link to={`/account/organization/${currentProject.orgId}`}>
-                    <i className='iconfont icon-organization' />
+                    <i className="iconfont icon-organization" />
                   </Link>
                 </Col>
               </Row>
@@ -311,12 +311,12 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
             <Box>
               <Box.Header>
                 <Box.Title>
-                  <Icon type='bars' />
+                  <Icon type="bars" />
                   数据资产列表
                 </Box.Title>
                 <Box.Tools>
-                  <Tooltip placement='bottom' title='新增'>
-                    <AdminButton type='primary' icon='plus' onClick={this.addView} />
+                  <Tooltip placement="bottom" title="新增">
+                    <AdminButton type="primary" icon="plus" onClick={this.addView} />
                   </Tooltip>
                 </Box.Tools>
               </Box.Header>
@@ -325,7 +325,7 @@ export class ViewList extends React.PureComponent<IViewListProps, IViewListState
                   <Col span={24}>
                     <Table
                       bordered
-                      rowKey='id'
+                      rowKey="id"
                       loading={loading.view}
                       dataSource={filterViews}
                       columns={tableColumns}
@@ -356,6 +356,7 @@ const mapDispatchToProps = (dispatch: Dispatch<ViewActionType>) => ({
   onLoadViews: (projectId) => dispatch(ViewActions.loadViews(projectId)),
   onDeleteView: (viewId, resolve) => dispatch(ViewActions.deleteView(viewId, resolve)),
   onCopyView: (view, resolve) => dispatch(ViewActions.copyView(view, resolve)),
+  // @ts-ignore
   onCheckName: (data, resolve, reject) => dispatch(checkNameUniqueAction('view', data, resolve, reject))
 })
 

+ 13 - 1
app/utils/api.ts

@@ -50,5 +50,17 @@ export default {
 
   // 根据ticket获取用户信息
   getUserInfo: `/taihu-auth/thirdLogin/getUserInfo`,
-  dataScreening: `/api/v3/dataScreening`
+  // 数据概览
+  dataScreening: `/api/v3/dataScreening`,
+  // 获取资源目录列表
+  getCatalogues: `/api/v3/catalogue/getCatalogues`,
+  // 创建资源目录 POST
+  createCatalogue: `/api/v3/catalogue/createCatalogue`,
+  // 修改资源目录 PUT /api/v3/catalogue/updateCatalogue/{id}
+  updateCatalogue: `/api/v3/catalogue/updateCatalogue`,
+  // 删除资源目录 DELETE /api/v3/catalogue/deleteCatalogue/{id}
+  deleteCatalogue: `/api/v3/catalogue/deleteCatalogue/`,
+  // 根据目录id获取view /api/v3/views/getViewsByParentId params:  projectId parentId
+  getViewsByParentId: `/api/v3/views/getViewsByParentId`,
+  // getCatalogues: ``,
 }