Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions frontend/src/components/datasources/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ export const RotatingChevron: React.FC<{ isExpanded: boolean }> = ({

export const DatasourceLabel: React.FC<{
children: React.ReactNode;
}> = ({ children }) => {
className?: string;
}> = ({ children, className }) => {
return (
<div className="flex gap-1.5 items-center font-bold px-2 py-1.5 text-muted-foreground bg-(--slate-2) text-sm">
<div
className={cn(
"flex gap-1.5 items-center font-bold py-1.5 text-muted-foreground bg-(--slate-2) text-sm",
className,
)}
>
{children}
</div>
);
Expand Down
104 changes: 81 additions & 23 deletions frontend/src/components/datasources/datasources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ import {
} 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",
tableLoading: "pl-11",
tableSchemaless: "pl-8",
tableWithSchema: "pl-12",
columnLocal: "pl-5",
columnSql: "pl-13",
columnPreview: "pl-10",
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idk if relevant anywhere, but this would make this constant readonly.

Suggested change
};
} as const;


const sortedTablesAtom = atom((get) => {
const tables = get(datasetTablesAtom);
const variables = get(variablesAtom);
Expand All @@ -98,7 +113,11 @@ const sortedTablesAtom = atom((get) => {
});
});

const connectionsAtom = atom((get) => {
/**
* 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
Expand Down Expand Up @@ -160,7 +179,7 @@ export const DataSources: React.FC = () => {
className="border-b bg-background rounded-none h-full pb-10 overflow-auto outline-hidden"
shouldFilter={false}
>
<div className="flex items-center w-full">
<div className="flex items-center w-full border-b">
<CommandInput
placeholder="Search tables..."
className="h-6 m-1"
Expand All @@ -170,7 +189,7 @@ export const DataSources: React.FC = () => {
closeAllColumns(value.length > 0);
setSearchValue(value);
}}
rootClassName="flex-1 border-r"
rootClassName="flex-1 border-r border-b-0"
/>
{hasSearch && (
<button
Expand All @@ -183,12 +202,13 @@ export const DataSources: React.FC = () => {
)}

<AddDatabaseDialog>
<button
type="button"
className="float-right border-b px-2 m-0 h-full hover:bg-accent hover:text-accent-foreground"
<Button
variant="ghost"
size="sm"
className="px-2 rounded-none focus-visible:outline-hidden"
>
<PlusIcon className="h-4 w-4" />
</button>
</Button>
</AddDatabaseDialog>
</div>

Expand Down Expand Up @@ -222,7 +242,7 @@ export const DataSources: React.FC = () => {
))}

{dataConnections.length > 0 && tables.length > 0 && (
<DatasourceLabel>
<DatasourceLabel className={INDENT.engine}>
<PythonIcon className="h-4 w-4 text-muted-foreground" />
<span className="text-xs">Python</span>
</DatasourceLabel>
Expand Down Expand Up @@ -258,7 +278,7 @@ const Engine: React.FC<{

return (
<>
<DatasourceLabel>
<DatasourceLabel className={INDENT.engine}>
<DatabaseLogo
className="h-4 w-4 text-muted-foreground"
name={connection.dialect}
Expand Down Expand Up @@ -288,7 +308,10 @@ const Engine: React.FC<{
{hasChildren ? (
children
) : (
<EmptyState content="No databases available" className="pl-2" />
<EmptyState
content="No databases available"
className={INDENT.engineEmpty}
/>
)}
</>
);
Expand All @@ -312,7 +335,10 @@ const DatabaseItem: React.FC<{
return (
<>
<CommandItem
className="text-sm flex flex-row gap-1 items-center cursor-pointer rounded-none"
className={cn(
"text-sm flex flex-row gap-1 items-center cursor-pointer rounded-none",
INDENT.database,
)}
onSelect={() => {
setIsExpanded(!isExpanded);
setIsSelected(!isSelected);
Expand Down Expand Up @@ -357,7 +383,12 @@ const SchemaList: React.FC<{
searchValue,
}) => {
if (schemas.length === 0) {
return <EmptyState content="No schemas available" className="pl-6" />;
return (
<EmptyState
content="No schemas available"
className={INDENT.schemaEmpty}
/>
);
}

const filteredSchemas = schemas.filter((schema) => {
Expand Down Expand Up @@ -413,7 +444,10 @@ const SchemaItem: React.FC<{
return (
<>
<CommandItem
className="text-sm flex flex-row gap-1 items-center pl-5 cursor-pointer rounded-none"
className={cn(
"text-sm flex flex-row gap-1 items-center cursor-pointer rounded-none",
INDENT.schema,
)}
onSelect={() => {
setIsExpanded(!isExpanded);
setIsSelected(!isSelected);
Expand Down Expand Up @@ -474,15 +508,22 @@ const TableList: React.FC<{
}, [tables.length, sqlTableContext, tablesRequested]);

if (isPending || tablesLoading) {
return <LoadingState message="Loading tables..." className="pl-12" />;
return (
<LoadingState
message="Loading tables..."
className={INDENT.tableLoading}
/>
);
}

if (error) {
return <ErrorState error={error} className="pl-12" />;
return <ErrorState error={error} className={INDENT.tableLoading} />;
}

if (tables.length === 0) {
return <EmptyState content="No tables found" className="pl-9" />;
return (
<EmptyState content="No tables found" className={INDENT.tableLoading} />
);
}

const filteredTables = tables.filter((table) => {
Expand Down Expand Up @@ -607,17 +648,27 @@ const DatasetTableItem: React.FC<{

const renderColumns = () => {
if (isPending || isFetching) {
return <LoadingState message="Loading columns..." className="pl-12" />;
return (
<LoadingState
message="Loading columns..."
className={INDENT.tableLoading}
/>
);
}

if (error) {
return <ErrorState error={error} className="pl-12" />;
return <ErrorState error={error} className={INDENT.tableLoading} />;
}

const columns = table.columns;

if (columns.length === 0) {
return <EmptyState content="No columns found" className="pl-12" />;
return (
<EmptyState
content="No columns found"
className={INDENT.tableLoading}
/>
);
}

return columns.map((column) => (
Expand Down Expand Up @@ -654,15 +705,17 @@ const DatasetTableItem: React.FC<{
className={cn(
"rounded-none group h-8 cursor-pointer",
sqlTableContext &&
(isSchemaless(sqlTableContext.schema) ? "pl-9" : "pl-12"),
(isSchemaless(sqlTableContext.schema)
? INDENT.tableSchemaless
: INDENT.tableWithSchema),
(isExpanded || isSearching) && "font-semibold",
)}
value={uniqueId}
aria-selected={isExpanded}
forceMount={true}
onSelect={() => setIsExpanded(!isExpanded)}
>
<div className="flex gap-2 items-center flex-1">
<div className="flex gap-2 items-center flex-1 pl-1">
{renderTableType()}
<span className="text-sm">{table.name}</span>
</div>
Expand Down Expand Up @@ -748,7 +801,7 @@ const DatasetColumnItem: React.FC<{
<div
className={cn(
"flex flex-row gap-2 items-center flex-1",
sqlTableContext ? "pl-14" : "pl-7",
sqlTableContext ? INDENT.columnSql : INDENT.columnLocal,
)}
>
<ColumnName columnName={columnText} dataType={column.type} />
Expand All @@ -775,7 +828,12 @@ const DatasetColumnItem: React.FC<{
</span>
</CommandItem>
{isExpanded && (
<div className="pl-10 pr-2 py-2 bg-(--slate-1) shadow-inner border-b">
<div
className={cn(
INDENT.columnPreview,
"pr-2 py-2 bg-(--slate-1) shadow-inner border-b",
)}
>
<ErrorBoundary>
<DatasetColumnPreview
table={table}
Expand Down
57 changes: 43 additions & 14 deletions frontend/src/components/editor/chrome/panels/session-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/* Copyright 2024 Marimo. All rights reserved. */
/* Copyright 2026 Marimo. All rights reserved. */

import { useAtom } from "jotai";
import { useAtom, useAtomValue } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { DatabaseIcon, VariableIcon } from "lucide-react";
import React from "react";
import { DataSources } from "@/components/datasources/datasources";
import React, { useCallback } from "react";
import {
connectionsAtom,
DataSources,
} from "@/components/datasources/datasources";
import {
Accordion,
AccordionContent,
Expand All @@ -14,41 +17,67 @@ import {
import { Badge } from "@/components/ui/badge";
import { VariableTable } from "@/components/variables/variables-table";
import { useCellIds } from "@/core/cells/cells";
import { useDatasets } from "@/core/datasets/state";
import { datasetTablesAtom } from "@/core/datasets/state";
import { useVariables } from "@/core/variables/state";
import { jotaiJsonStorage } from "@/utils/storage/jotai";

const openSectionsAtom = atomWithStorage<string[]>(
"marimo:session-panel:open-sections",
["variables"],
type OpenSections = "variables" | "datasources";

interface SessionPanelState {
openSections: OpenSections[];
hasUserInteracted: boolean;
}

const sessionPanelAtom = atomWithStorage<SessionPanelState>(
"marimo:session-panel:state",
{ openSections: ["variables"], hasUserInteracted: false },
jotaiJsonStorage,
);

const SessionPanel: React.FC = () => {
const variables = useVariables();
const cellIds = useCellIds();
const datasets = useDatasets();
const [openSections, setOpenSections] = useAtom(openSectionsAtom);
const tables = useAtomValue(datasetTablesAtom);
const dataConnections = useAtomValue(connectionsAtom);
const [state, setState] = useAtom(sessionPanelAtom);

const datasourcesCount = tables.length + dataConnections.length;

// If the user hasn't interacted with the accordion and there are connections, show datasources open
const openSections =
!state.hasUserInteracted && datasourcesCount > 0
? [...new Set([...state.openSections, "datasources"])]
: state.openSections;

const handleValueChange = useCallback(
(value: OpenSections[]) => {
setState({
openSections: value,
hasUserInteracted: true,
});
},
[setState],
);

const datasourcesCount = datasets.tables.length;
const isDatasourcesOpen = openSections.includes("datasources");
const showDatasourcesBadge = !isDatasourcesOpen && datasourcesCount > 0;

return (
<Accordion
type="multiple"
value={openSections}
onValueChange={setOpenSections}
onValueChange={handleValueChange}
className="flex flex-col h-full overflow-auto"
>
<AccordionItem value="datasources" className="border-b">
<AccordionTrigger className="px-3 py-2 text-xs font-semibold uppercase tracking-wide hover:no-underline">
<span className="flex items-center gap-2">
<DatabaseIcon className="w-4 h-4" />
Data sources
{!isDatasourcesOpen && datasourcesCount > 0 && (
{showDatasourcesBadge && (
<Badge
variant="secondary"
className="ml-1 px-1.5 py-0 text-[10px]"
className="ml-1 px-1.5 py-0 mb-px text-[10px]"
>
{datasourcesCount}
</Badge>
Expand Down
Loading