/** * 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 } from '@apache-superset/core/theme'; import { t } from '@apache-superset/core/translation'; import type { NodeRendererProps } from 'react-arborist'; import { Icons, Tooltip, Typography } from '@superset-ui/core/components'; import RefreshLabel from '@superset-ui/core/components/RefreshLabel'; import ColumnElement from 'src/SqlLab/components/ColumnElement'; import IconButton from 'src/dashboard/components/IconButton'; import type { TreeNodeData, FetchLazyTablesParams } from './types'; const StyledColumnNode = styled.div` & > .ant-flex { flex: 1; margin-right: ${({ theme }) => theme.sizeUnit * 1.5}px; cursor: default; } `; 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; fetchLazyTables: (params: FetchLazyTablesParams) => void; handlePinTable: ( tableName: string, schemaName: string, catalogName: string | null, ) => void; } const TreeNodeRenderer: React.FC = ({ node, style, manuallyOpenedNodes, loadingNodes, searchTerm, catalog, fetchLazyTables, handlePinTable, }) => { const { data } = node; const parts = data.id.split(':'); const [identifier, _dbId, schema, tableName] = parts; // Use manually tracked open state for icon display // This prevents search auto-expansion from affecting the icon const isManuallyOpen = manuallyOpenedNodes[data.id] ?? false; 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.EyeOutlined : Icons.TableOutlined; // Show loading icon with table type icon when loading if (isLoading) { return ( <> ); } const ExpandIcon = isManuallyOpen ? Icons.MinusSquareOutlined : Icons.PlusSquareOutlined; 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()} > ); } return (
{ e.stopPropagation(); if (node.isLeaf) { node.select(); } else { node.toggle(); } }} > {renderIcon()} {highlightText(data.name, searchTerm)} {identifier === 'schema' && (
{ e.stopPropagation(); fetchLazyTables({ dbId: _dbId, catalog, schema, forceRefresh: true, }); }} tooltipContent={t('Force refresh table list')} />
)} {identifier === 'table' && (
} onClick={e => { e.stopPropagation(); handlePinTable(tableName, schema, catalog ?? null); }} />
)}
); }; export default TreeNodeRenderer;