/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 { css, styled, useTheme } from '@apache-superset/core/theme'; import { t } from '@apache-superset/core/translation'; import type { NodeRendererProps } from 'react-arborist'; import { Icons, Typography } from '@superset-ui/core/components'; import RefreshLabel from '@superset-ui/core/components/RefreshLabel'; import ColumnElement from 'src/SqlLab/components/ColumnElement'; import { ActionButton } from '@superset-ui/core/components/ActionButton'; import copyTextToClipboard from 'src/utils/copy'; import type { TreeNodeData } from './types'; const StyledColumnNode = styled.div` & > .ant-flex { flex: 1; margin-right: ${({ theme }) => theme.sizeUnit * 4}px; cursor: default; } .col-copy-action { opacity: 0; flex-shrink: 0; margin-left: ${({ theme }) => theme.sizeUnit}px; } &:hover .col-copy-action { opacity: 1; } `; const getOpacity = (disableCheckbox: boolean | undefined) => disableCheckbox ? 0.6 : 1; const highlightText = (text: string, keyword: string): React.ReactNode => { if (!keyword) { return text; } const lowerText = text.toLowerCase(); const lowerKeyword = keyword.toLowerCase(); const index = lowerText.indexOf(lowerKeyword); if (index === -1) { return text; } const beforeStr = text.substring(0, index); const matchStr = text.substring(index, index + keyword.length); const afterStr = text.slice(index + keyword.length); return ( <> {beforeStr} {matchStr} {afterStr} ); }; export interface TreeNodeRendererProps extends NodeRendererProps { manuallyOpenedNodes: Record; loadingNodes: Record; searchTerm: string; catalog: string | null | undefined; pinnedTableKeys: Set; pinnedSchemas: Set; selectStarMap: Record; handleRefreshTables: (params: { dbId: number; catalog: string | null | undefined; schema: string; }) => void; handlePinTable: ( tableName: string, schemaName: string, catalogName: string | null, ) => void; handleUnpinTable: (tableName: string, schemaName: string) => void; handlePinSchema: (schemaName: string) => void; handleUnpinSchema: (schemaName: string) => void; refreshTableSchema: (id: string) => void; sortedTables: Record; toggleSortColumns: (tableId: string) => void; } const TreeNodeRenderer: React.FC = ({ node, style, loadingNodes, searchTerm, catalog, pinnedTableKeys, pinnedSchemas, selectStarMap, handleRefreshTables, handlePinTable, handleUnpinTable, handlePinSchema, handleUnpinSchema, refreshTableSchema, sortedTables, toggleSortColumns, }) => { const theme = useTheme(); const { data } = node; const parts = data.id.split(':'); const [identifier, _dbId, schema, tableName] = parts; const isManuallyOpen = node.isOpen && !node.data.disableCheckbox; const isLoading = loadingNodes[data.id] ?? false; const renderIcon = () => { if (identifier === 'schema') { // Show loading icon when fetching data for schema if (isLoading) { return ; } return isManuallyOpen ? ( ) : ( ); } if (identifier === 'table') { const TableTypeIcon = data.tableType === 'view' ? Icons.FunctionOutlined : Icons.TableOutlined; if (isLoading) { return ; } return ; } return null; }; // Empty placeholder node - no actions allowed if (data.type === 'empty') { return (
{data.name}
); } // Column nodes use ColumnElement if (identifier === 'column' && data.columnData) { return ( node.select()} > e.stopPropagation()} > } onClick={() => copyTextToClipboard(() => Promise.resolve(data.name)) } /> } /> ); } return (
{ e.stopPropagation(); if (node.isLeaf) { node.select(); } else { node.toggle(); } }} > {renderIcon()} {highlightText(data.name, searchTerm)} {identifier === 'schema' && (
e.stopPropagation()} > {pinnedSchemas.has(schema) && (
} onClick={() => handleUnpinSchema(schema)} />
)}
{ e.stopPropagation(); handleRefreshTables({ dbId: Number(_dbId), catalog, schema, }); }} tooltipContent={t('Force refresh table list')} /> ) : ( ) } onClick={() => pinnedSchemas.has(schema) ? handleUnpinSchema(schema) : handlePinSchema(schema) } />
)} {identifier === 'table' && (() => { const nodeDbId = Number(_dbId); const tableKey = `${nodeDbId}:${schema}:${tableName}`; const isPinned = pinnedTableKeys.has(tableKey); const selectStar = selectStarMap[tableKey]; return (
e.stopPropagation()} > {isPinned && (
} onClick={() => handleUnpinTable(tableName, schema)} />
)}
{selectStar && ( } onClick={() => copyTextToClipboard(() => Promise.resolve(selectStar)) } /> )} } onClick={() => toggleSortColumns(data.id)} /> } onClick={() => refreshTableSchema(data.id)} /> ) : ( ) } onClick={() => isPinned ? handleUnpinTable(tableName, schema) : handlePinTable(tableName, schema, catalog ?? null) } /> ) : ( ) } onClick={() => node.toggle()} />
); })()}
); }; export default TreeNodeRenderer;