From fe91b806afa1114ddad894ea9ac638d359fbdd75 Mon Sep 17 00:00:00 2001 From: Shahmir Varqha Date: Thu, 28 May 2026 17:34:52 +0800 Subject: [PATCH] add a toggle to filter empty schemas and db --- .../__tests__/filter-empty.test.ts | 93 ++++++++++++++++++ .../components/datasources/datasources.tsx | 98 ++++++++++++++++++- marimo/_sql/engines/ibis.py | 6 -- 3 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/datasources/__tests__/filter-empty.test.ts diff --git a/frontend/src/components/datasources/__tests__/filter-empty.test.ts b/frontend/src/components/datasources/__tests__/filter-empty.test.ts new file mode 100644 index 00000000000..2b07ec6e850 --- /dev/null +++ b/frontend/src/components/datasources/__tests__/filter-empty.test.ts @@ -0,0 +1,93 @@ +/* Copyright 2026 Marimo. All rights reserved. */ + +import { describe, expect, it } from "vitest"; +import type { Database, DataTable } from "@/core/kernel/messages"; +import { filterEmptyDatabases } from "../datasources"; + +function makeTable(name: string): DataTable { + return { + name, + columns: [], + source: "memory", + source_type: "local", + type: "table", + engine: null, + indexes: null, + num_columns: null, + num_rows: null, + variable_name: null, + primary_keys: null, + }; +} + +function makeDatabase( + name: string, + schemas: Array<{ name: string; tables: DataTable[] }>, +): Database { + return { + name, + dialect: "duckdb", + schemas, + engine: null, + }; +} + +describe("filterEmptyDatabases", () => { + it("hides schemas with no tables", () => { + const databases = [ + makeDatabase("memory", [ + { name: "main", tables: [makeTable("t1")] }, + { name: "empty_schema", tables: [] }, + ]), + ]; + + expect(filterEmptyDatabases(databases)).toEqual([ + makeDatabase("memory", [{ name: "main", tables: [makeTable("t1")] }]), + ]); + }); + + it("hides databases where every schema is empty", () => { + const databases = [ + makeDatabase("only_empty", [ + { name: "a", tables: [] }, + { name: "b", tables: [] }, + ]), + makeDatabase("has_tables", [{ name: "main", tables: [makeTable("t1")] }]), + ]; + + expect(filterEmptyDatabases(databases)).toEqual([ + makeDatabase("has_tables", [{ name: "main", tables: [makeTable("t1")] }]), + ]); + }); + + it("preserves databases with no schemas (lazy state)", () => { + const databases = [makeDatabase("not_loaded_yet", [])]; + + expect(filterEmptyDatabases(databases)).toEqual([ + makeDatabase("not_loaded_yet", []), + ]); + }); + + it("returns an empty list when all databases are empty", () => { + const databases = [ + makeDatabase("a", [{ name: "main", tables: [] }]), + makeDatabase("b", [{ name: "main", tables: [] }]), + ]; + + expect(filterEmptyDatabases(databases)).toEqual([]); + }); + + it("does not mutate the input", () => { + const databases = [ + makeDatabase("memory", [ + { name: "main", tables: [makeTable("t1")] }, + { name: "empty_schema", tables: [] }, + ]), + ]; + const snapshot = JSON.parse(JSON.stringify(databases)); + + filterEmptyDatabases(databases); + + expect(databases).toEqual(snapshot); + }); +}); diff --git a/frontend/src/components/datasources/datasources.tsx b/frontend/src/components/datasources/datasources.tsx index 8b7ef3029a8..0cf8b50f298 100644 --- a/frontend/src/components/datasources/datasources.tsx +++ b/frontend/src/components/datasources/datasources.tsx @@ -1,8 +1,15 @@ /* Copyright 2026 Marimo. All rights reserved. */ import { CommandList } from "cmdk"; -import { atom, useAtomValue, useSetAtom } from "jotai"; -import { PlusIcon, PlusSquareIcon, XIcon } from "lucide-react"; +import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; +import { + EyeIcon, + EyeOffIcon, + PlusIcon, + PlusSquareIcon, + XIcon, +} from "lucide-react"; import React from "react"; import { dbDisplayName } from "@/components/databases/display"; import { EngineVariable } from "@/components/databases/engine-variable"; @@ -52,6 +59,7 @@ import { sortBy } from "@/utils/arrays"; import { logNever } from "@/utils/assertNever"; import { cn } from "@/utils/cn"; import { Events } from "@/utils/events"; +import { jotaiJsonStorage } from "@/utils/storage/jotai"; import { DatabaseIcon, SchemaIcon, @@ -116,6 +124,51 @@ const sortedTablesAtom = atom((get) => { }); }); +/** + * Whether to hide empty schemas and databases (those with no tables) in the + * datasources panel. + */ +export const hideEmptyDatasourcesAtom = atomWithStorage( + "marimo:datasources:hideEmpty", + false, + jotaiJsonStorage, + { getOnInit: true }, +); + +/** + * Apply the "hide empty" filter to a connection's databases. + * + * - Schemas with no tables are hidden. + * - Databases are hidden when they have at least one schema and every schema + * is empty. + * - Databases with no schemas yet (lazy state) are preserved so users can + * still expand them to trigger a schema fetch. + */ +export function filterEmptyDatabases(databases: Database[]): Database[] { + let changed = false; + const result: Database[] = []; + for (const database of databases) { + if (database.schemas.length === 0) { + result.push(database); + continue; + } + const nonEmptySchemas = database.schemas.filter( + (schema) => schema.tables.length > 0, + ); + if (nonEmptySchemas.length === 0) { + changed = true; + continue; + } + if (nonEmptySchemas.length === database.schemas.length) { + result.push(database); + continue; + } + changed = true; + result.push({ ...database, schemas: nonEmptySchemas }); + } + return changed ? result : databases; +} + /** * This atom is used to get the data connections that are available to the user. * It filters out the internal engines if it has no databases or if it has only the in-memory database and no schemas. @@ -152,10 +205,27 @@ export const connectionsAtom = atom((get) => { export const DataSources: React.FC = () => { const [searchValue, setSearchValue] = React.useState(""); + const [hideEmpty, setHideEmpty] = useAtom(hideEmptyDatasourcesAtom); const closeAllColumns = useSetAtom(closeAllColumnsAtom); const tables = useAtomValue(sortedTablesAtom); - const dataConnections = useAtomValue(connectionsAtom); + const rawConnections = useAtomValue(connectionsAtom); + + const dataConnections = React.useMemo(() => { + if (!hideEmpty) { + return rawConnections; + } + let changed = false; + const filtered = rawConnections.map((connection) => { + const databases = filterEmptyDatabases(connection.databases); + if (databases === connection.databases) { + return connection; + } + changed = true; + return { ...connection, databases }; + }); + return changed ? filtered : rawConnections; + }, [rawConnections, hideEmpty]); if (tables.length === 0 && dataConnections.length === 0) { return ( @@ -204,6 +274,28 @@ export const DataSources: React.FC = () => { )} + + + +