Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
68487ef
Minor Tailwind CSS linting fixes
NuroDev Feb 10, 2026
7f43d5c
Minor Tailwind CSS linting fixes
NuroDev Feb 10, 2026
334acbe
Added initial D1 route plumbing
NuroDev Feb 10, 2026
43a6b47
Added `lodash` dependency
NuroDev Feb 10, 2026
ae30d73
Added `@types/lodash` dev dependency
NuroDev Feb 10, 2026
c7deb47
Add initial (D1) driver plumbing
NuroDev Feb 10, 2026
e1526b4
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 10, 2026
d3082c0
Added changeset
NuroDev Feb 10, 2026
87bb21c
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 10, 2026
c0bef86
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 10, 2026
50d44ec
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 10, 2026
93ac650
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 11, 2026
ca30e3f
Removed placeholder driver logic from D1 database route
NuroDev Feb 11, 2026
fb7bbf7
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 11, 2026
22a69e6
Minor refactoring
NuroDev Feb 11, 2026
09133d9
Minor `StudioSQLiteExplainTab` refactoring
NuroDev Feb 11, 2026
f3187e7
Minor SQLite driver generator refactoring
NuroDev Feb 11, 2026
39c88ec
Improve driver JSDoc's
NuroDev Feb 11, 2026
1e7e857
Removed `mysql` dialect support
NuroDev Feb 11, 2026
12138db
Minor refactoring / code cleanup
NuroDev Feb 11, 2026
2e810b3
Added explicit returns to `CursorV2`
NuroDev Feb 11, 2026
e26192d
Fixed Devin critical suggestions
NuroDev Feb 11, 2026
355e6b9
Fixed Devin minor suggestions
NuroDev Feb 11, 2026
44b98b8
Replaced `Promise.all` with `Promise.allSettled`
NuroDev Feb 11, 2026
0c86976
Improved data fetching error message
NuroDev Feb 11, 2026
5e9789f
Added granular data fetching error handling
NuroDev Feb 11, 2026
589b7bd
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 11, 2026
0f61c9c
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 11, 2026
31dd02b
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 11, 2026
3cd4e28
Fix `TOKEN_IDENTIFIER` & `TOKEN_STRING_LITERAL` regex pattern parenth…
NuroDev Feb 11, 2026
4525660
[C3] bump sv from 0.11.4 to 0.12.1 in /packages/create-cloudflare/src…
dependabot[bot] Feb 11, 2026
cbd2e8c
fix(wrangler): don't proxy localhost requests during dev (#12516)
edmundhung Feb 11, 2026
9378280
[wrangler] Use project's package manager in wrangler setup (#12437)
MattieTK Feb 11, 2026
17fdca7
Skip TurboRepo running builds in Vite playground packages (#12450)
jamesopstad Feb 11, 2026
ac29fe9
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 11, 2026
23656ba
Refactored sidebar to have re-usable item group component
NuroDev Feb 12, 2026
1bafa14
Refactored to use a unified breadcrumb component
NuroDev Feb 12, 2026
82536b7
Added basic unit tests for SQLite driver + utils
NuroDev Feb 12, 2026
a82a064
Fixed React fragment missing key
NuroDev Feb 12, 2026
b516b8d
Replaced `lodash` with inline forks
NuroDev Feb 12, 2026
fa79a07
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 12, 2026
690d78a
Improved changeset description
NuroDev Feb 12, 2026
bcb4087
Merge branch 'main' into NuroDev/local-explorer-studio-plumbing
NuroDev Feb 12, 2026
d934801
Replaced `String(...)` with `JSON.stringify(...)`
NuroDev Feb 12, 2026
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
9 changes: 9 additions & 0 deletions .changeset/chilly-pans-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/local-explorer-ui": minor
---

Implemented initial data studio driver support.

This provides the initial plumbing needed to add the complete data studio component to the local explorer in a later PR.

This is an experimental WIP feature.
2 changes: 2 additions & 0 deletions packages/local-explorer-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@phosphor-icons/react": "^2.1.10",
"@tailwindcss/vite": "^4.0.15",
"@tanstack/react-router": "^1.158.0",
"lodash": "^4.17.23",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwindcss": "^4.0.15"
Expand All @@ -30,6 +31,7 @@
"@hey-api/openapi-ts": "^0.91.1",
"@tanstack/react-router-devtools": "^1.158.0",
"@tanstack/router-plugin": "^1.158.0",
"@types/lodash": "^4.17.23",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^4.4.1",
Expand Down
87 changes: 70 additions & 17 deletions packages/local-explorer-ui/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import { Collapsible } from "@base-ui/react/collapsible";
import { cn } from "@cloudflare/kumo";
import { CaretRightIcon } from "@phosphor-icons/react";
import { CaretRightIcon, DatabaseIcon } from "@phosphor-icons/react";
import { Link } from "@tanstack/react-router";
import CloudflareLogo from "../assets/icons/cloudflare-logo.svg?react";
import KVIcon from "../assets/icons/kv.svg?react";
import type { WorkersKvNamespace } from "../api";
import type { D1DatabaseResponse, WorkersKvNamespace } from "../api";

interface SidebarProps {
namespaces: WorkersKvNamespace[];
loading: boolean;
error: string | null;
currentPath: string;
d1Error: string | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

should we have a single error banner? instead of making it resource specific.

i see the pr description says you made it more granular but i'm just wondering why

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I thought about this as well. The only reason I made these errors unique is so if (for whatever reason) D1 databases fail to load but KV does, you can still access your local KV namespaces and not have the sidebar just set all items to render an error.

Copy link
Contributor

Choose a reason for hiding this comment

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

What about setting a single error, and returning an empty array for the problematic resource if there is an error?

databases: D1DatabaseResponse[];
kvError: string | null;
loading: boolean;
namespaces: WorkersKvNamespace[];
}

export function Sidebar({
namespaces,
loading,
error,
currentPath,
d1Error,
databases,
kvError,
loading,
namespaces,
}: SidebarProps) {
return (
<aside className="w-sidebar bg-bg-secondary border-r border-border flex flex-col">
<a
className="flex items-center gap-2.5 p-4 border-b border-border min-h-[67px] box-border"
className="flex items-center gap-2.5 p-4 border-b border-border min-h-16.75 box-border"
href="/"
>
<CloudflareLogo className="shrink-0 text-primary" />
Expand All @@ -31,31 +35,31 @@ export function Sidebar({
Local Explorer
</span>
<span className="text-[10px] font-medium text-text-secondary uppercase tracking-wide">
Cloudflare Dev Tools
Cloudflare DevTools
</span>
</div>
</a>

<Collapsible.Root defaultOpen>
<Collapsible.Trigger className="group flex items-center gap-2 w-full py-3 px-4 border-0 border-b border-border bg-transparent font-semibold text-[11px] uppercase tracking-wide text-text-secondary cursor-pointer transition-colors hover:bg-border">
<CaretRightIcon className="transition-transform duration-200 group-data-[panel-open]:rotate-90" />
<CaretRightIcon className="transition-transform duration-200 group-data-panel-open:rotate-90" />
<KVIcon className="w-3.5 h-3.5" />
KV Namespaces
</Collapsible.Trigger>
<Collapsible.Panel className="overflow-hidden transition-[height,opacity] duration-200 ease-out data-[starting-style]:h-0 data-[starting-style]:opacity-0 data-[ending-style]:h-0 data-[ending-style]:opacity-0">
<Collapsible.Panel className="overflow-hidden transition-[height,opacity] duration-200 ease-out data-starting-style:h-0 data-starting-style:opacity-0 data-ending-style:h-0 data-ending-style:opacity-0">
<ul className="list-none flex-1 overflow-y-auto">
{loading && (
<li className="block py-2.5 px-4 text-text-secondary border-b border-border">
Loading...
</li>
)}
{error && (
{kvError && (
<li className="block py-2.5 px-4 text-danger border-b border-border">
{error}
{kvError}
</li>
)}
{!loading &&
!error &&
!kvError &&
namespaces.map((ns) => {
const isActive = currentPath === `/kv/${ns.id}`;
return (
Expand All @@ -66,7 +70,7 @@ export function Sidebar({
className={cn(
"block py-2.5 px-4 text-text no-underline border-b border-border cursor-pointer transition-colors hover:bg-border",
isActive
? "bg-primary/8 text-primary border-l-3 border-l-primary pl-[13px]"
? "bg-primary/8 text-primary border-l-3 border-l-primary pl-3.25"
: ""
)}
>
Expand All @@ -75,14 +79,63 @@ export function Sidebar({
</li>
);
})}
{!loading && !error && namespaces.length === 0 && (
{!loading && !kvError && namespaces.length === 0 && (
<li className="block py-2.5 px-4 text-text-secondary border-b border-border">
No namespaces
</li>
)}
</ul>
</Collapsible.Panel>
</Collapsible.Root>

<Collapsible.Root defaultOpen>
<Collapsible.Trigger className="group flex items-center gap-2 w-full py-3 px-4 border-0 border-b border-border bg-transparent font-semibold text-[11px] uppercase tracking-wide text-text-secondary cursor-pointer transition-colors hover:bg-border">
<CaretRightIcon className="transition-transform duration-200 group-data-panel-open:rotate-90" />
<DatabaseIcon className="w-3.5 h-3.5" />
D1 Databases
</Collapsible.Trigger>
<Collapsible.Panel className="overflow-hidden transition-[height,opacity] duration-200 ease-out data-starting-style:h-0 data-starting-style:opacity-0 data-ending-style:h-0 data-ending-style:opacity-0">
<ul className="list-none flex-1 overflow-y-auto">
{loading && (
<li className="block py-2.5 px-4 text-text-secondary border-b border-border">
Loading...
</li>
)}
{d1Error && (
<li className="block py-2.5 px-4 text-danger border-b border-border">
{d1Error}
</li>
)}
{!loading &&
!d1Error &&
databases.map((database) => {
const isActive = currentPath === `/d1/${database.uuid}`;
return (
<li key={database.uuid}>
<Link
className={cn(
"block py-2.5 px-4 text-text no-underline border-b border-border cursor-pointer transition-colors hover:bg-border",
isActive
? "bg-primary/8 text-primary border-l-3 border-l-primary pl-3.25"
: ""
)}
params={{ databaseId: database.uuid as string }}
search={{ table: undefined }}
to="/d1/$databaseId"
>
{database.name}
</Link>
</li>
);
})}
{!loading && !d1Error && databases.length === 0 && (
<li className="block py-2.5 px-4 text-text-secondary border-b border-border">
No databases
</li>
)}
</ul>
</Collapsible.Panel>
</Collapsible.Root>
</aside>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { cn, Tooltip } from "@cloudflare/kumo";
import { TableIcon } from "@phosphor-icons/react";
import { Fragment } from "react";
import type { StudioResultSet } from "../../../types/studio";
import type { ReactNode } from "react";

interface StudioSQLiteExplainProps {
data: StudioResultSet;
}

interface StudioSQLiteExplainRow {
id: number;
parent: number;
detail: string;
}

interface StudioSQLiteExplainTree extends StudioSQLiteExplainRow {
children: StudioSQLiteExplainTree[];
}

export function StudioSQLiteExplainTab({
data,
}: StudioSQLiteExplainProps): JSX.Element {
const rows = data.rows as unknown as StudioSQLiteExplainRow[];

let tree = rows.map(
(r) =>
({
...r,
children: [],
}) satisfies StudioSQLiteExplainTree
);

const nodeTable = tree.reduce(
(a, b) => ({
...a,
[b.id]: b,
}),
{} as Record<string, StudioSQLiteExplainTree>
);

for (const node of tree) {
if (node.parent) {
nodeTable[node.parent]?.children.push(node);
}
}

tree = tree.filter((node) => node.parent === 0);

return (
<div className="w-full h-full grow p-8 overflow-auto">
<div className="font-mono text-sm">
<ExplainNodes data={tree} />
</div>
</div>
);
}

interface ExplainNodesProps {
data: StudioSQLiteExplainTree[];
}

function ExplainNodes({ data }: ExplainNodesProps): JSX.Element {
return (
<>
{data.map((row) => {
const { label, performance } = describeExplainNode(row.detail);

return (
<Fragment key={row.id}>
<div className="h-8 flex gap-2 items-center">
<div
className={cn("inline-flex border rounded-full", {
"bg-green-500": performance === "fast",
"bg-red-500": performance === "slow",
"bg-yellow-500": performance === "medium",
"bg-gray-500": performance === "neutral",
})}
style={{ width: 10, height: 10, marginLeft: -5 }}
/>
<div>{label}</div>
</div>
<div className="pl-4 border-l">
<ExplainNodes data={row.children} />
</div>
</Fragment>
);
})}
</>
);
}

type ExplainNodePerformance = "slow" | "medium" | "fast" | "neutral";

/**
* Convert an EXPLAIN step detail string into a UI-friendly
* description with performance classification and formatted label.
*
* Performance indicates execution efficiency:
* - slow: likely very costly
* - medium: potentially adds extra work
* - fast: generally efficient
* - neutral: informational only
*
* @param detail - The raw detail text from the EXPLAIN result
*
* @returns Object containing a ReactNode label and performance level
*/
function describeExplainNode(d: string): {
label: ReactNode;
performance: ExplainNodePerformance;
} {
if (d.startsWith("SCAN ")) {
return {
performance: "slow",
label: (
<div className="flex items-center">
<strong>SCAN </strong>
<span className="border border-color p-1 mx-2 rounded flex items-center gap-2">
<TableIcon />
{d.substring("SCAN ".length)}
</span>
</div>
),
};
}

if (d.startsWith("CORRELATED ")) {
return {
performance: "slow",
label: (
<div>
<Tooltip
side="bottom"
content={
<div className="flex flex-col gap-2">
<div>
This subquery depends on values from the outer query, so
it&apos;s evaluated once per outer row.{" "}
<strong className="text-red-500">
Can be slow on large inputs
</strong>
.
</div>
<div className="text-green-500">
Mitigate by indexing the correlated columns or rewriting as a
JOIN + aggregate.
</div>
</div>
}
>
<strong className="underline cursor-pointer">CORRELATED</strong>
</Tooltip>
<span>{d.substring("CORRELATED".length)}</span>
</div>
),
};
}

if (d.startsWith("SEARCH ")) {
return {
performance: "fast",
label: (
<div>
<strong>SEARCH </strong>
<span>{d.substring("SEARCH".length)}</span>
</div>
),
};
}

if (
d.startsWith("USE TEMP B-TREE FOR ORDER BY") ||
d.startsWith("USE TEMP B-TREE FOR GROUP BY") ||
d.startsWith("USE TEMP B-TREE FOR DISTINCT")
) {
return {
performance: "medium",
label: (
<Tooltip
side="bottom"
content={
<div className="flex flex-col gap-2">
<div>
SQLite can’t return rows in the requested order/grouping
directly, so it gathers them into a temporary structure and
processes them before returning results.{" "}
<span className="text-red-500">
This adds extra work and grows with result size.
</span>
</div>
<div className="text-green-500">
Add an index that matches the clause (ORDER BY / GROUP BY /
DISTINCT) to avoid the temp structure.
</div>
</div>
}
>
<strong className="underline cursor-pointer">{d}</strong>
</Tooltip>
),
};
}

return {
label: <span>{d}</span>,
performance: "neutral",
};
}
Loading
Loading