Files
superset2/superset-frontend/src/SqlLab/components/TableExploreTree/TreeNodeRenderer.tsx
Superset Dev 0e32b0399f chore(lint): upgrade no-unused-vars rule from warn to error
Flip @typescript-eslint/no-unused-vars from "warn" to "error" in
oxlint.json (with argsIgnorePattern/varsIgnorePattern of "^_" and
caughtErrors "none"), and clean up the resulting violations across the
frontend.

Fixes favor proper cleanup over underscore hacks: genuinely dead
imports, variables, and destructured props are removed; parameters are
only "_"-prefixed when their position is required by an external or
callback signature (event handlers, render props, antd validators,
forwardRef, or omit-from-rest destructuring).

Note: the .eslintrc.js half of the prior attempt (#37886) is obsolete
since the frontend migrated to oxlint as the primary linter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-16 15:48:06 -07:00

409 lines
12 KiB
TypeScript

/**
* 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}
<span className="highlighted">{matchStr}</span>
{afterStr}
</>
);
};
export interface TreeNodeRendererProps extends NodeRendererProps<TreeNodeData> {
manuallyOpenedNodes: Record<string, boolean>;
loadingNodes: Record<string, boolean>;
searchTerm: string;
catalog: string | null | undefined;
pinnedTableKeys: Set<string>;
pinnedSchemas: Set<string>;
selectStarMap: Record<string, string>;
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<string, boolean>;
toggleSortColumns: (tableId: string) => void;
}
const TreeNodeRenderer: React.FC<TreeNodeRendererProps> = ({
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 <Icons.LoadingOutlined iconSize="l" />;
}
return isManuallyOpen ? (
<Icons.FolderOpenOutlined iconSize="l" />
) : (
<Icons.FolderOutlined iconSize="l" />
);
}
if (identifier === 'table') {
const TableTypeIcon =
data.tableType === 'view'
? Icons.FunctionOutlined
: Icons.TableOutlined;
if (isLoading) {
return <Icons.LoadingOutlined iconSize="l" />;
}
return <TableTypeIcon iconSize="l" />;
}
return null;
};
// Empty placeholder node - no actions allowed
if (data.type === 'empty') {
return (
<div
className="tree-node"
style={{
...style,
opacity: 0.5,
fontStyle: 'italic',
cursor: 'default',
}}
>
<span className="tree-node-icon">
<Icons.FileOutlined iconSize="l" />
</span>
<span className="tree-node-title">{data.name}</span>
</div>
);
}
// Column nodes use ColumnElement
if (identifier === 'column' && data.columnData) {
return (
<StyledColumnNode
className="tree-node"
style={style}
data-selected={node.isSelected}
onClick={() => node.select()}
>
<ColumnElement
column={data.columnData}
actions={
<span
className="col-copy-action"
onClick={e => e.stopPropagation()}
>
<ActionButton
label={`copy-col-${data.name}`}
tooltip={t('Copy column name')}
icon={<Icons.CopyOutlined iconSize="m" />}
onClick={() =>
copyTextToClipboard(() => Promise.resolve(data.name))
}
/>
</span>
}
/>
</StyledColumnNode>
);
}
return (
<div
className="tree-node"
style={style}
data-selected={node.isSelected}
onClick={e => {
e.stopPropagation();
if (node.isLeaf) {
node.select();
} else {
node.toggle();
}
}}
>
<span
className="tree-node-icon"
css={css`
opacity: ${getOpacity(data.disableCheckbox)};
`}
>
{renderIcon()}
</span>
<Typography.Text
className="tree-node-title"
css={css`
opacity: ${getOpacity(data.disableCheckbox)};
`}
ellipsis={{
tooltip: { title: data.name, placement: 'topLeft' },
}}
>
{highlightText(data.name, searchTerm)}
</Typography.Text>
{identifier === 'schema' && (
<div
className="side-action-container"
role="menu"
onClick={e => e.stopPropagation()}
>
{pinnedSchemas.has(schema) && (
<div className="action-static">
<ActionButton
label={`pinned-schema-${schema}`}
icon={
<Icons.PushpinFilled
iconSize="m"
css={css`
color: ${theme.colorTextDescription};
`}
/>
}
onClick={() => handleUnpinSchema(schema)}
/>
</div>
)}
<div className="action-hover">
<RefreshLabel
onClick={e => {
e.stopPropagation();
handleRefreshTables({
dbId: Number(_dbId),
catalog,
schema,
});
}}
tooltipContent={t('Force refresh table list')}
/>
<ActionButton
label={
pinnedSchemas.has(schema)
? `unpin-schema-${schema}`
: `pin-schema-${schema}`
}
tooltip={
pinnedSchemas.has(schema)
? t('Unpin from top')
: t('Pin to top')
}
icon={
pinnedSchemas.has(schema) ? (
<Icons.PushpinFilled iconSize="m" />
) : (
<Icons.PushpinOutlined iconSize="m" />
)
}
onClick={() =>
pinnedSchemas.has(schema)
? handleUnpinSchema(schema)
: handlePinSchema(schema)
}
/>
</div>
</div>
)}
{identifier === 'table' &&
(() => {
const nodeDbId = Number(_dbId);
const tableKey = `${nodeDbId}:${schema}:${tableName}`;
const isPinned = pinnedTableKeys.has(tableKey);
const selectStar = selectStarMap[tableKey];
return (
<div
className="side-action-container"
role="menu"
onClick={e => e.stopPropagation()}
>
{isPinned && (
<div className="action-static">
<ActionButton
label={`pinned-${schema}-${tableName}`}
icon={
<Icons.PushpinFilled
iconSize="m"
css={css`
color: ${theme.colorTextDescription};
`}
/>
}
onClick={() => handleUnpinTable(tableName, schema)}
/>
</div>
)}
<div className="action-hover">
{selectStar && (
<ActionButton
label={`copy-select-${schema}-${tableName}`}
tooltip={t('Copy SELECT statement to the clipboard')}
icon={<Icons.CopyOutlined iconSize="m" />}
onClick={() =>
copyTextToClipboard(() => Promise.resolve(selectStar))
}
/>
)}
<ActionButton
label={`sort-cols-${schema}-${tableName}`}
tooltip={
sortedTables[data.id]
? t('Sort by original table order')
: t('Sort columns alphabetically')
}
icon={
<Icons.SortAscendingOutlined
iconSize="m"
css={css`
color: ${sortedTables[data.id]
? theme.colorPrimary
: 'inherit'};
`}
/>
}
onClick={() => toggleSortColumns(data.id)}
/>
<ActionButton
label={`refresh-schema-${schema}-${tableName}`}
tooltip={t('Refresh table schema')}
icon={<Icons.SyncOutlined iconSize="m" />}
onClick={() => refreshTableSchema(data.id)}
/>
<ActionButton
label={
isPinned
? `unpin-${schema}-${tableName}`
: `pin-${schema}-${tableName}`
}
tooltip={
isPinned
? t('Unpin from the result panel')
: t('Pin to the result panel')
}
icon={
isPinned ? (
<Icons.PushpinFilled iconSize="m" />
) : (
<Icons.PushpinOutlined iconSize="m" />
)
}
onClick={() =>
isPinned
? handleUnpinTable(tableName, schema)
: handlePinTable(tableName, schema, catalog ?? null)
}
/>
<ActionButton
label={`toggle-${schema}-${tableName}`}
tooltip={isManuallyOpen ? t('Collapse') : t('Expand')}
icon={
isManuallyOpen ? (
<Icons.UpOutlined iconSize="m" />
) : (
<Icons.DownOutlined iconSize="m" />
)
}
onClick={() => node.toggle()}
/>
</div>
</div>
);
})()}
</div>
);
};
export default TreeNodeRenderer;