diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/constants.ts b/superset-frontend/src/components/Datasource/FoldersEditor/constants.ts index fc9f4d189f8..913ee905fb6 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/constants.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/constants.ts @@ -29,6 +29,9 @@ export const DEFAULT_METRICS_FOLDER_UUID = export const DEFAULT_COLUMNS_FOLDER_UUID = '83a7ae8f-2e8a-4f2b-a8cb-ebaebef95b9b'; +// Number of default folders (Metrics, Columns) +export const DEFAULT_FOLDERS_COUNT = 2; + // Drag & drop constants export const DRAG_INDENTATION_WIDTH = 64; export const MAX_DEPTH = 3; diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.test.ts b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.test.ts index c6fdfa444b7..b17b7b727fe 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.test.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.test.ts @@ -28,8 +28,10 @@ import { removeChildrenOf, serializeForAPI, getProjection, + countAllFolders, } from './treeUtils'; import { FoldersEditorItemType } from '../types'; +import { DatasourceFolder } from 'src/explore/components/DatasourcePanel/types'; const createMetricItem = (uuid: string, name: string): TreeItem => ({ uuid, @@ -667,3 +669,60 @@ test('getProjection nests item under folder when dragging down with offset', () expect(projection!.depth).toBe(1); expect(projection!.parentId).toBe('folder1'); }); + +test('countAllFolders returns 0 for empty array', () => { + expect(countAllFolders([])).toBe(0); +}); + +test('countAllFolders counts flat folders', () => { + const folders: DatasourceFolder[] = [ + createFolderItem('f1', 'Metrics', [createMetricItem('m1', 'Metric 1')]), + createFolderItem('f2', 'Columns', [createColumnItem('c1', 'Column 1')]), + ] as DatasourceFolder[]; + + expect(countAllFolders(folders)).toBe(2); +}); + +test('countAllFolders counts nested folders recursively', () => { + const folders: DatasourceFolder[] = [ + createFolderItem('metrics', 'Metrics', [ + createMetricItem('m1', 'Metric 1'), + ]), + createFolderItem('columns', 'Columns', [ + createColumnItem('c1', 'Column 1'), + ]), + createFolderItem('custom', 'Custom Folder', [ + createFolderItem('nested', 'Nested Folder', [ + createMetricItem('m2', 'Metric 2'), + ]), + ]), + ] as DatasourceFolder[]; + + expect(countAllFolders(folders)).toBe(4); +}); + +test('countAllFolders counts deeply nested folders', () => { + const folders: DatasourceFolder[] = [ + createFolderItem('level0', 'Level 0', [ + createFolderItem('level1', 'Level 1', [ + createFolderItem('level2', 'Level 2', [ + createMetricItem('m1', 'Metric 1'), + ]), + ]), + ]), + ] as DatasourceFolder[]; + + expect(countAllFolders(folders)).toBe(3); +}); + +test('countAllFolders ignores non-folder children', () => { + const folders: DatasourceFolder[] = [ + createFolderItem('f1', 'Folder', [ + createMetricItem('m1', 'Metric 1'), + createColumnItem('c1', 'Column 1'), + createMetricItem('m2', 'Metric 2'), + ]), + ] as DatasourceFolder[]; + + expect(countAllFolders(folders)).toBe(1); +}); diff --git a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts index 10dec0d28ab..c35817606d9 100644 --- a/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts +++ b/superset-frontend/src/components/Datasource/FoldersEditor/treeUtils.ts @@ -330,3 +330,22 @@ export function serializeForAPI(items: TreeItem[]): DatasourceFolder[] { }) .filter((folder): folder is DatasourceFolder => folder !== null); } + +/** + * Recursively counts all folders in a DatasourceFolder array, + * including nested sub-folders within children. + */ +export function countAllFolders(folders: DatasourceFolder[]): number { + let count = 0; + for (const folder of folders) { + count += 1; + if (folder.children) { + for (const child of folder.children) { + if ('children' in child) { + count += countAllFolders([child as DatasourceFolder]); + } + } + } + } + return count; +} diff --git a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx index c7819929074..fae377266a6 100644 --- a/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx +++ b/superset-frontend/src/components/Datasource/components/DatasourceEditor/DatasourceEditor.tsx @@ -91,9 +91,11 @@ import { fetchSyncedColumns, updateColumns } from '../../utils'; import DatasetUsageTab from './components/DatasetUsageTab'; import { DEFAULT_COLUMNS_FOLDER_UUID, + DEFAULT_FOLDERS_COUNT, DEFAULT_METRICS_FOLDER_UUID, } from '../../FoldersEditor/constants'; import { validateFolders } from '../../FoldersEditor/folderValidation'; +import { countAllFolders } from '../../FoldersEditor/treeUtils'; import FoldersEditor from '../../FoldersEditor'; import { DatasourceFolder } from 'src/explore/components/DatasourcePanel/types'; @@ -266,6 +268,7 @@ interface DatasourceEditorState { databaseColumns: Column[]; calculatedColumns: Column[]; folders: DatasourceFolder[]; + folderCount: number; metadataLoading: boolean; activeTabKey: string; datasourceType: string; @@ -284,6 +287,7 @@ interface AbortControllers { interface CollectionTabTitleProps { title: string; collection?: unknown[] | { length: number }; + count?: number; } interface ColumnCollectionTableProps { @@ -458,6 +462,7 @@ DATASOURCE_TYPES_ARR.forEach(o => { function CollectionTabTitle({ title, collection, + count, }: CollectionTabTitleProps): JSX.Element { return (