/* Copyright 2026 Marimo. All rights reserved. */ import { CommandList } from "cmdk"; import { atom, useAtomValue, useSetAtom } from "jotai"; import { PlusIcon, PlusSquareIcon, XIcon } from "lucide-react"; import React from "react"; import { dbDisplayName } from "@/components/databases/display"; import { EngineVariable } from "@/components/databases/engine-variable"; import { DatabaseLogo } from "@/components/databases/icon"; import { RefreshIconButton } from "@/components/editor/file-tree/tree-actions"; import { CopyClipboardIcon } from "@/components/icons/copy-icon"; import { Button } from "@/components/ui/button"; import { Command, CommandInput, CommandItem } from "@/components/ui/command"; import { Tooltip } from "@/components/ui/tooltip"; import { maybeAddMarimoImport } from "@/core/cells/add-missing-import"; import { cellIdsAtom, useCellActions } from "@/core/cells/cells"; import { useLastFocusedCellId } from "@/core/cells/focus"; import { autoInstantiateAtom } from "@/core/config/config"; import { dataConnectionsMapAtom, type SQLTableContext, useDataSourceActions, } from "@/core/datasets/data-source-connections"; import { DEFAULT_DUCKDB_DATABASE, DUCKDB_ENGINE, INTERNAL_SQL_ENGINES, } from "@/core/datasets/engines"; import { PreviewSQLSchemaList, PreviewSQLTable, PreviewSQLTableList, } from "@/core/datasets/request-registry"; import { closeAllColumnsAtom, datasetTablesAtom, expandedColumnsAtom, useDatasets, } from "@/core/datasets/state"; import type { Database, DatabaseSchema, DataSourceConnection, DataTable, DataTableColumn, } from "@/core/kernel/messages"; import { useRequestClient } from "@/core/network/requests"; import { variablesAtom } from "@/core/variables/state"; import type { VariableName } from "@/core/variables/types"; import { useAsyncData } from "@/hooks/useAsyncData"; import { sortBy } from "@/utils/arrays"; import { logNever } from "@/utils/assertNever"; import { cn } from "@/utils/cn"; import { Events } from "@/utils/events"; import { DatabaseIcon, SchemaIcon, TableIcon, ViewIcon, } from "../databases/namespace-icons"; import { ErrorBoundary } from "../editor/boundary/ErrorBoundary"; import { PythonIcon } from "../editor/cell/code/icons"; import { useAddCodeToNewCell } from "../editor/cell/useAddCell"; import { PanelEmptyState } from "../editor/chrome/panels/empty-state"; import { AddConnectionDialog } from "../editor/connections/add-connection-dialog"; import { DatasetColumnPreview } from "./column-preview"; import { ColumnName, DatasourceLabel, EmptyState, ErrorState, LoadingState, RotatingChevron, } from "./components"; import { isSchemaless, sqlCode } from "./utils"; // Indentation classes for the datasource tree hierarchy. const INDENT = { engineEmpty: "pl-3", engine: "pl-3 pr-2", database: "pl-4", schemaEmpty: "pl-8", schema: "pl-7", schemaLoading: "pl-8", tableLoading: "pl-11", tableSchemaless: "pl-8", tableWithSchema: "pl-12", columnLocal: "pl-5", columnSql: "pl-13", columnPreview: "pl-10", }; const sortedTablesAtom = atom((get) => { const tables = get(datasetTablesAtom); const variables = get(variablesAtom); const cellIds = get(cellIdsAtom); // Sort tables by the index of the variable they are defined in return sortBy(tables, (table) => { // Put at the top if (!table.variable_name) { return -1; } const variable = Object.values(variables).find( (v) => v.name === table.variable_name, ); if (!variable) { return 0; } const index = cellIds.inOrderIds.indexOf(variable.declaredBy[0]); if (index === -1) { return 0; } return index; }); }); /** * 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. */ export const connectionsAtom = atom((get) => { const dataConnections = new Map(get(dataConnectionsMapAtom)); // Filter out the internal engines if it has no databases // Or if it has only the in-memory database and no schemas for (const engine of INTERNAL_SQL_ENGINES) { const connection = dataConnections.get(engine); if (!connection) { continue; } if (connection.databases.length === 0) { dataConnections.delete(engine); } if ( connection.databases.length === 1 && connection.databases[0].name === DEFAULT_DUCKDB_DATABASE && connection.databases[0].schemas.length === 0 ) { dataConnections.delete(engine); } } // Put internal engines last to prioritize user-defined connections return sortBy([...dataConnections.values()], (connection) => INTERNAL_SQL_ENGINES.has(connection.name) ? 1 : 0, ); }); export const DataSources: React.FC = () => { const [searchValue, setSearchValue] = React.useState(""); const closeAllColumns = useSetAtom(closeAllColumnsAtom); const tables = useAtomValue(sortedTablesAtom); const dataConnections = useAtomValue(connectionsAtom); if (tables.length === 0 && dataConnections.length === 0) { return ( } icon={} /> ); } const hasSearch = !!searchValue.trim(); return (
{ // If searching, remove open previews closeAllColumns(value.length > 0); setSearchValue(value); }} rootClassName="flex-1 border-r border-b-0" /> {hasSearch && ( )}
{dataConnections.map((connection) => ( 0} > {connection.databases.map((database) => ( ))} ))} {dataConnections.length > 0 && tables.length > 0 && ( Python )} {tables.length > 0 && ( )}
); }; const Engine: React.FC<{ connection: DataSourceConnection; children: React.ReactNode; hasChildren?: boolean; }> = ({ connection, children, hasChildren }) => { // The internal duckdb connection is updated automatically, so we do not need to refresh. const internalEngine = connection.name === DUCKDB_ENGINE; const engineName = internalEngine ? "In-Memory" : connection.name; const { previewDataSourceConnection } = useRequestClient(); const handleRefreshConnection = async () => { await previewDataSourceConnection({ engine: connection.name, }); }; return ( <> {dbDisplayName(connection.dialect)} () {!internalEngine && ( )} {hasChildren ? ( children ) : ( )} ); }; const DatabaseItem: React.FC<{ hasSearch: boolean; engineName: string; database: Database; children: React.ReactNode; }> = ({ hasSearch, engineName, database, children }) => { const [isExpanded, setIsExpanded] = React.useState(false); const [isSelected, setIsSelected] = React.useState(false); const [prevHasSearch, setPrevHasSearch] = React.useState(hasSearch); if (prevHasSearch !== hasSearch) { setPrevHasSearch(hasSearch); setIsExpanded(hasSearch); } return ( <> { setIsExpanded(!isExpanded); setIsSelected(!isSelected); }} value={`${engineName}:${database.name}`} > {database.name === "" ? Not connected : database.name} {isExpanded && children} ); }; const SchemaList: React.FC<{ schemas: DatabaseSchema[]; defaultSchema?: string | null; defaultDatabase?: string | null; dialect: string; engineName: string; databaseName: string; hasSearch: boolean; searchValue?: string; }> = ({ schemas, defaultSchema, defaultDatabase, dialect, engineName, databaseName, hasSearch, searchValue, }) => { const { addSchemaList } = useDataSourceActions(); const [schemasRequested, setSchemasRequested] = React.useState(false); // Custom loading state, we need to wait for the data to propagate once requested // useAsyncData's loading state may return false before data has propagated const [schemasLoading, setSchemasLoading] = React.useState(false); const { isPending, error } = useAsyncData(async () => { if (schemas.length === 0 && engineName && !schemasRequested) { setSchemasRequested(true); setSchemasLoading(true); try { const previewSchemaList = await PreviewSQLSchemaList.request({ engine: engineName, database: databaseName, }); addSchemaList({ schemas: previewSchemaList.schemas ?? [], sqlSchemaContext: { engine: engineName, database: databaseName, }, }); } finally { setSchemasLoading(false); } } }, [schemas.length, engineName, databaseName, schemasRequested]); if (isPending || schemasLoading) { return ( ); } if (error) { return ; } if (schemas.length === 0) { return ( ); } const filteredSchemas = schemas.filter((schema) => { if (searchValue) { return schema.tables.some((table) => table.name.toLowerCase().includes(searchValue.toLowerCase()), ); } return true; }); return ( <> {filteredSchemas.map((schema) => ( ))} ); }; const SchemaItem: React.FC<{ databaseName: string; schema: DatabaseSchema; children: React.ReactNode; hasSearch: boolean; }> = ({ databaseName, schema, children, hasSearch }) => { const [isExpanded, setIsExpanded] = React.useState(hasSearch); const [isSelected, setIsSelected] = React.useState(false); const uniqueValue = `${databaseName}:${schema.name}`; if (isSchemaless(schema.name)) { return children; } return ( <> { setIsExpanded(!isExpanded); setIsSelected(!isSelected); }} value={uniqueValue} > {schema.name} {isExpanded && children} ); }; const TableList: React.FC<{ tables: DataTable[]; sqlTableContext?: SQLTableContext; searchValue?: string; }> = ({ tables, sqlTableContext, searchValue }) => { const { addTableList } = useDataSourceActions(); const [tablesRequested, setTablesRequested] = React.useState(false); // Custom loading state, we need to wait for the data to propagate once requested // useAsyncData's loading state may return false before data has propagated const [tablesLoading, setTablesLoading] = React.useState(false); const { isPending, error } = useAsyncData(async () => { if (tables.length === 0 && sqlTableContext && !tablesRequested) { setTablesRequested(true); setTablesLoading(true); const { engine, database, schema } = sqlTableContext; const previewTableList = await PreviewSQLTableList.request({ engine: engine, database: database, schema: schema, }); if (!previewTableList?.tables) { setTablesLoading(false); throw new Error("No tables available"); } addTableList({ tables: previewTableList.tables, sqlTableContext: sqlTableContext, }); setTablesLoading(false); } }, [tables.length, sqlTableContext, tablesRequested]); if (isPending || tablesLoading) { return ( ); } if (error) { return ; } if (tables.length === 0) { return ( ); } const filteredTables = tables.filter((table) => { if (searchValue) { return table.name.toLowerCase().includes(searchValue.toLowerCase()); } return true; }); return ( <> {filteredTables.map((table) => ( ))} ); }; const DatasetTableItem: React.FC<{ table: DataTable; sqlTableContext?: SQLTableContext; isSearching: boolean; }> = ({ table, sqlTableContext, isSearching }) => { const { addTable } = useDataSourceActions(); const [isExpanded, setIsExpanded] = React.useState(false); const [tableDetailsRequested, setTableDetailsRequested] = React.useState(false); const tableDetailsExist = table.columns.length > 0; const { isFetching, isPending, error } = useAsyncData(async () => { if ( isExpanded && !tableDetailsExist && sqlTableContext && !tableDetailsRequested ) { setTableDetailsRequested(true); const { engine, database, schema } = sqlTableContext; const previewTable = await PreviewSQLTable.request({ engine: engine, database: database, schema: schema, tableName: table.name, }); if (!previewTable?.table) { throw new Error("No table details available"); } addTable({ table: previewTable.table, sqlTableContext: sqlTableContext, }); } }, [isExpanded, tableDetailsExist]); const autoInstantiate = useAtomValue(autoInstantiateAtom); const lastFocusedCellId = useLastFocusedCellId(); const { createNewCell } = useCellActions(); const addCodeToNewCell = useAddCodeToNewCell(); const handleAddTable = () => { maybeAddMarimoImport({ autoInstantiate, createNewCell, fromCellId: lastFocusedCellId, }); const getCode = () => { if (table.source_type === "catalog") { const identifier = sqlTableContext?.database ? `${sqlTableContext.database}.${table.name}` : table.name; return `${table.engine}.load_table("${identifier}")`; } if (sqlTableContext) { return sqlCode({ table, columnName: "*", sqlTableContext }); } switch (table.source_type) { case "local": return `mo.ui.table(${table.name})`; case "duckdb": case "connection": return sqlCode({ table, columnName: "*", sqlTableContext }); default: logNever(table.source_type); return ""; } }; addCodeToNewCell(getCode()); }; const renderRowsByColumns = () => { const label: string[] = []; if (table.num_rows != null) { label.push(`${table.num_rows} rows`); } if (table.num_columns != null) { label.push(`${table.num_columns} columns`); } if (label.length === 0) { return null; } return (
{label.join(", ")}
); }; const renderColumns = () => { if (isPending || isFetching) { return ( ); } if (error) { return ; } const columns = table.columns; if (columns.length === 0) { return ( ); } return columns.map((column) => ( )); }; const renderTableType = () => { if (table.source_type === "local") { return; } const TableTypeIcon = table.type === "table" ? TableIcon : ViewIcon; return ( ); }; const uniqueId = sqlTableContext ? `${sqlTableContext.database}.${sqlTableContext.schema}.${table.name}` : table.name; return ( <> setIsExpanded(!isExpanded)} >
{renderTableType()} {table.name}
{renderRowsByColumns()}
{isExpanded && renderColumns()} ); }; const DatasetColumnItem: React.FC<{ table: DataTable; column: DataTableColumn; sqlTableContext?: SQLTableContext; }> = ({ table, column, sqlTableContext }) => { const [isExpanded, setIsExpanded] = React.useState(false); const closeAllColumns = useAtomValue(closeAllColumnsAtom); const setExpandedColumns = useSetAtom(expandedColumnsAtom); if (closeAllColumns && isExpanded) { setIsExpanded(false); } if (isExpanded) { setExpandedColumns( (prev) => new Set([...prev, `${table.name}:${column.name}`]), ); } else { setExpandedColumns((prev) => { prev.delete(`${table.name}:${column.name}`); return new Set(prev); }); } const addCodeToNewCell = useAddCodeToNewCell(); const { columnsPreviews } = useDatasets(); const isPrimaryKey = table.primary_keys?.includes(column.name) || false; const isIndexed = table.indexes?.includes(column.name) || false; const handleAddColumn = (chartCode: string) => { addCodeToNewCell(chartCode); }; const renderItemSubtext = ({ tooltipContent, content, }: { tooltipContent: string; content: string; }) => { return ( {content} ); }; const columnText = ( {column.name} ); return ( <> setIsExpanded(!isExpanded)} >
{isPrimaryKey && renderItemSubtext({ tooltipContent: "Primary key", content: "PK" })} {isIndexed && renderItemSubtext({ tooltipContent: "Indexed", content: "IDX" })}
{column.external_type}
{isExpanded && (
)} ); };