/* Copyright 2026 Marimo. All rights reserved. */
import { Provider, useAtomValue } from "jotai";
import { DatabaseZapIcon } from "lucide-react";
import type React from "react";
import { useEffect } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { useLocale } from "react-aria";
import { ProtocolIcon } from "@/components/storage/components";
import { Badge } from "@/components/ui/badge";
import { cacheInfoAtom } from "@/core/cache/requests";
import { dataConnectionsMapAtom } from "@/core/datasets/data-source-connections";
import type { ConnectionName } from "@/core/datasets/engines";
import { datasetTablesAtom } from "@/core/datasets/state";
import { getRequestClient } from "@/core/network/requests";
import { store } from "@/core/state/jotai";
import { storageNamespacesAtom } from "@/core/storage/state";
import { variablesAtom } from "@/core/variables/state";
import type { VariableName } from "@/core/variables/types";
import { assertNever } from "@/utils/assertNever";
import { formatTime } from "@/utils/formatting";
import { prettyNumber } from "@/utils/numbers";
import {
CONTAINER_STYLES,
MetadataRow,
renderDatasourceInfo,
renderEmptyInfo,
renderTableInfo,
SectionHeader,
} from "../language/languages/sql/renderers";
import type { CodeLensSpec } from "./entities";
/**
* Mounts the hover popover for a code lens into `dom`; returns a dispose
* function.
*/
export function mountLensPopover(
dom: HTMLElement,
spec: CodeLensSpec,
): () => void {
const root = createRoot(dom);
// CodeMirror measures and positions the tooltip as soon as this function
// returns. Render now so it measures the popover content, not an empty
// container that later grows over the hovered icon.
flushSync(() => {
root.render(
,
);
});
// Defer so tearing the tooltip down from inside a React event (e.g. the
// icon's click handler) doesn't unmount mid-render
return () => queueMicrotask(() => root.unmount());
}
const LensPopover: React.FC<{ spec: CodeLensSpec }> = ({ spec }) => {
switch (spec.kind) {
case "table":
return ;
case "connection":
return ;
case "bucket":
return ;
case "cache":
return (
);
default:
assertNever(spec.kind);
}
};
const TablePopover: React.FC<{ name: string }> = ({ name }) => {
const tables = useAtomValue(datasetTablesAtom);
const table = tables.find((t) => t.variable_name === name);
return table ? renderTableInfo(table) : renderEmptyInfo("table");
};
const ConnectionPopover: React.FC<{ name: string }> = ({ name }) => {
const connections = useAtomValue(dataConnectionsMapAtom);
const connection = connections.get(name as ConnectionName);
if (!connection) {
return renderEmptyInfo("database");
}
return renderDatasourceInfo(connection);
};
const BucketPopover: React.FC<{ name: string }> = ({ name }) => {
const namespaces = useAtomValue(storageNamespacesAtom);
const namespace = namespaces.find((ns) => ns.name === name);
if (!namespace) {
return (
No storage information available.
);
}
return (
}
title={namespace.displayName}
badge={
{namespace.protocol}
}
/>
{namespace.name}
}
/>
{namespace.rootPath || "(root)"}
}
/>
{namespace.backendType}}
/>
);
};
// `_cache_call.__repr__` starts with "hits=N misses=N", which survives the
// variable-preview truncation
const CACHE_STATS_PATTERN = /\bhits=(\d+) misses=(\d+)/;
// The popover remounts on every hover (see `mountLensPopover`), so this has
// to live at module scope rather than in component state: it debounces the
// notebook-wide `getCacheInfo` fetch across repeated hovers.
let lastCacheInfoFetchAt: number | null = null;
const CACHE_INFO_REFETCH_INTERVAL_MS = 5000;
function maybeFetchCacheInfo(): void {
const now = Date.now();
if (
lastCacheInfoFetchAt != null &&
now - lastCacheInfoFetchAt < CACHE_INFO_REFETCH_INTERVAL_MS
) {
return;
}
lastCacheInfoFetchAt = now;
getRequestClient()
.getCacheInfo()
.catch(() => {
// Allow the next hover to retry rather than waiting out the interval.
lastCacheInfoFetchAt = null;
});
}
const CachePopover: React.FC<{
boundName: string | null;
cacheName: string | null;
}> = ({ boundName, cacheName }) => {
const { locale } = useLocale();
const variables = useAtomValue(variablesAtom);
const cacheInfo = useAtomValue(cacheInfoAtom);
const variable = boundName ? variables[boundName as VariableName] : undefined;
const stats = variable?.dataType?.startsWith("_cache_call")
? variable.value?.match(CACHE_STATS_PATTERN)
: undefined;
const hasLocalStats = Boolean(stats);
// Only fetch the notebook-wide fallback when there are no per-cache stats
// to show, and throttle across repeated hovers so mousing over a lens
// several times in a row doesn't fire a fresh request every time.
useEffect(() => {
if (hasLocalStats) {
return;
}
maybeFetchCacheInfo();
}, [hasLocalStats]);
return (
}
title={cacheName ?? boundName ?? "Cache"}
/>
{stats ? (
{prettyNumber(Number(stats[1]), locale)}
}
/>
{prettyNumber(Number(stats[2]), locale)}
}
/>
) : (
cacheInfo && (
Notebook-wide
)
)}
Click to open the cache panel
);
};