/* Copyright 2026 Marimo. All rights reserved. */ import { HashIcon, InfoIcon } from "lucide-react"; import type React from "react"; import { dbDisplayName } from "@/components/databases/display"; import { ColumnIcon, DatabaseIcon, DatasourceIcon, IndexIcon, PrimaryKeyIcon, SchemaIcon, TableIcon, ViewIcon, } from "@/components/databases/namespace-icons"; import { DATA_TYPE_ICON } from "@/components/datasets/icons"; import { Badge } from "@/components/ui/badge"; import { type ConnectionName, INTERNAL_SQL_ENGINES, } from "@/core/datasets/engines"; import type { Database, DatabaseSchema, DataSourceConnection, DataTable, DataTableColumn, DataType, } from "@/core/kernel/messages"; import { PluralWord } from "@/utils/pluralize"; // Configuration constants const PREVIEW_ITEM_LIMIT = 5; // Color mappings for data types (Tailwind-safe) const DATA_TYPE_COLORS: Record = { boolean: "bg-[var(--orange-4)] text-[var(--orange-11)]", date: "bg-[var(--grass-4)] text-[var(--grass-11)]", time: "bg-[var(--grass-4)] text-[var(--grass-11)]", datetime: "bg-[var(--grass-4)] text-[var(--grass-11)]", number: "bg-[var(--purple-4)] text-[var(--purple-11)]", integer: "bg-[var(--purple-4)] text-[var(--purple-11)]", string: "bg-[var(--blue-4)] text-[var(--blue-11)]", unknown: "bg-[var(--slate-4)] text-[var(--slate-11)]", }; // Source type colors const SOURCE_TYPE_COLORS = { local: "bg-[var(--blue-4)] text-[var(--blue-11)]", duckdb: "bg-[var(--amber-4)] text-[var(--amber-11)]", connection: "bg-[var(--green-4)] text-[var(--green-11)]", catalog: "bg-[var(--purple-4)] text-[var(--purple-11)]", } as const; const CONTAINER_STYLES = "p-3 min-w-[250px] flex flex-col divide-y"; const columnsText = new PluralWord("column", "columns"); const rowsText = new PluralWord("row", "rows"); const schemasText = new PluralWord("schema", "schemas"); const tablesText = new PluralWord("table", "tables"); const databasesText = new PluralWord("database", "databases"); // Helper components and functions const SectionHeader: React.FC<{ icon: React.ReactNode; title: string; badge?: React.ReactNode; }> = ({ icon, title, badge }) => (
{icon}

{title}

{badge}
); const MetadataRow: React.FC<{ label: string; value: React.ReactNode; }> = ({ label, value }) => (
{label}: {value}
); const StatisticItem: React.FC<{ icon: React.ReactNode; text: string; }> = ({ icon, text }) => (
{icon} {text}
); const PreviewList: React.FC<{ title?: string; items: React.ReactNode[]; totalCount: number; limit?: number; }> = ({ title = "", items, totalCount, limit = PREVIEW_ITEM_LIMIT }) => { if (items.length === 0) { return null; } const visibleItems = items.slice(0, limit); const hasMore = totalCount > limit; return (
{title && (

{title}:

)}
{visibleItems} {hasMore && (
... and {totalCount - limit} more
)}
); }; const getDataTypeColorClass = (dataType: DataType): string => { return DATA_TYPE_COLORS[dataType] || DATA_TYPE_COLORS.unknown; }; export const renderTableInfo = (table: DataTable): React.ReactNode => { const tableIcon = table.type === "view" ? ( ) : ( ); const typeBadge = ( {table.type} ); const columnItems = table.columns.map((column) => { const TypeIcon = DATA_TYPE_ICON[column.type]; return (
{column.name}
{column.type}
); }); const hasPrimaryKeys = table.primary_keys && table.primary_keys.length > 0; const hasIndexes = table.indexes && table.indexes.length > 0; return (
{/* Metadata */}
{table.source} } /> {table.variable_name && ( {table.variable_name} } /> )} {table.engine && ( {table.engine} } /> )}
{/* Statistics */} {(table.num_columns != null || table.num_rows != null) && (
{table.num_columns != null && ( } text={`${table.num_columns} ${columnsText.pluralize(table.num_columns)}`} /> )} {table.num_rows != null && ( } text={`${table.num_rows} ${rowsText.pluralize(table.num_rows)}`} /> )}
)} {/* Empty Info */} {table.columns.length === 0 && renderEmptyInfo("column")} {/* Primary Keys & Indexes */} {(hasPrimaryKeys || hasIndexes) && (
{hasPrimaryKeys && (
Primary Keys:
{table.primary_keys?.map((key) => ( {key} ))}
)} {hasIndexes && (
Indexes:
{table.indexes?.map((index) => ( {index} ))}
)}
)} {/* Sample Columns Preview */} {table.columns.length > 0 && ( )}
); }; export const renderColumnInfo = (column: DataTableColumn): React.ReactNode => { const TypeIcon = DATA_TYPE_ICON[column.type]; const typeBadge = ( {column.type} ); const sampleItems = column.sample_values?.map((value, index) => (
{value === null || value === undefined ? "null" : String(value)}
)) || []; return (
} title={column.name} badge={typeBadge} /> {/* Type Information */}
{column.type}} /> {column.external_type} } />
{/* Sample Values */} {column.sample_values && column.sample_values.length > 0 && ( )}
); }; export const renderDatabaseInfo = (database: Database): React.ReactNode => { const dialectBadge = ( {database.dialect} ); const schemaItems = database.schemas.map((schema) => (
{schema.name}
{schema.tables.length} {tablesText.pluralize(schema.tables.length)}
)); return (
} title={database.name} badge={dialectBadge} /> {/* Metadata */}
{database.dialect}} /> {database.engine && ( {database.engine} } /> )}
{/* Schema Statistics */}
} text={`${database.schemas.length} schema${database.schemas.length === 1 ? "" : "s"}`} />
{/* Empty Info */} {database.schemas.length === 0 && renderEmptyInfo("schema")} {/* Schema Preview */} {database.schemas.length > 0 && ( )}
); }; export const renderSchemaInfo = (schema: DatabaseSchema): React.ReactNode => { const schemaBadge = ( Schema ); const tableItems = schema.tables.map((table) => (
{table.type === "view" ? ( ) : ( )} {table.name}
{table.type}
)); return (
} title={schema.name} badge={schemaBadge} /> {/* Table Statistics */}
} text={`${schema.tables.length} table${schema.tables.length === 1 ? "" : "s"}`} />
{/* Empty Info */} {schema.tables.length === 0 && renderEmptyInfo("table")} {/* Table Preview */} {schema.tables.length > 0 && ( )}
); }; const DefaultBadge = default; const MAX_SCHEMAS_TO_DISPLAY = 8; const MAX_TABLES_TO_DISPLAY = 3; export const renderDatasourceInfo = ( connection: DataSourceConnection, dataframes?: DataTable[], ): React.ReactNode => { const databaseCount = connection.databases.length; const schemasCount = connection.databases.reduce( (count, db) => count + db.schemas.length, 0, ); const renderSchema = (schema: DatabaseSchema, isDefaultDb: boolean) => { if (schema.tables.length === 0) { return null; } const isDefaultSchema = schema.name === connection.default_schema && isDefaultDb; let tableItems: React.ReactNode[] = []; // Don't display table items if there are many schemas if (schemasCount < MAX_SCHEMAS_TO_DISPLAY) { tableItems = schema.tables .slice(0, MAX_TABLES_TO_DISPLAY + 1) .map((table) => { return (
{table.name}
); }); } return (
{schema.name} {isDefaultSchema && DefaultBadge} {schema.tables.length} tables
); }; const databaseItems = connection.databases.map((db) => { const isDefaultDb = db.name === connection.default_database || connection.databases.length === 1; const schemaItems = db.schemas.map((schema) => renderSchema(schema, isDefaultDb), ); return (
{db.name} {isDefaultDb && DefaultBadge}
{schemaItems && ( )}
); }); let title = connection.name; if (INTERNAL_SQL_ENGINES.has(connection.name as ConnectionName)) { title = "In-Memory"; } const dataframeItems = dataframes?.map((table) => (
{table.name}
)); return (
} title={title} /> {/* Metadata */}
{dbDisplayName(connection.dialect)} } /> {connection.source} } />
{/* Statistics */}
} text={`${databaseCount} ${databasesText.pluralize(databaseCount)}`} /> } text={`${schemasCount} ${schemasText.pluralize(schemasCount)}`} />
{/* Database Preview */} {databaseCount > 0 && ( )} {/* Tables Preview */} {dataframeItems && dataframeItems.length > 0 && ( )}
); }; export const renderEmptyInfo = ( type: "column" | "table" | "schema" | "database", ) => { return (
No {type} information available.{" \n"} Introspect to see more details.
); };