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 { public state: ISourceTableStates = { filterKeyword: '', expandedNodeKeys: [], autoExpandTable: true } private inputChange = (propName: keyof IView) => (e: React.ChangeEvent) => { 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 = private iconTable = private iconDate = private iconKey = private iconText = private iconValue = 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 ( $1`) }} /> ) } 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() 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 = ( ) 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 ({columnNodes}) }) const nodes = Object.values(databasesInfo).length === 1 ? tableNodes : ({tableNodes}) databaseNodes.push(nodes) return databaseNodes }, []) return treeNodes } ) private loadTreeData = (node: AntTreeNode) => new Promise((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) => { const filterKeyword = e.target.value const { schema, view } = this.props const { mapTables, mapColumns } = schema const expandedNodeKeys = new Set() 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 (
{this.renderTableColumns(sourceId, schema, filterKeyword, onDatabaseSelect)}
) } } export default SourceTable