Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 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
30 changes: 30 additions & 0 deletions packages/local-explorer-ui/src/components/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CaretRightIcon } from "@phosphor-icons/react";
import type { FC } from "react";

interface BreadcrumbsProps {
icon: FC;
title: string;
items: Array<string>;
}

export function Breadcrumbs({
icon: Icon,
items,
title,
}: BreadcrumbsProps): JSX.Element {
return (
<div className="flex items-center gap-2 py-4 px-6 min-h-16.75 box-border bg-bg-secondary border-b border-border text-sm shrink-0">
<span className="flex items-center gap-1.5">
<Icon />
{title}
</span>

{items.map((item) => (
<>
<CaretRightIcon className="w-4 h-4" />
<span className="flex items-center gap-1.5">{item}</span>
</>
))}
</div>
);
}
187 changes: 130 additions & 57 deletions packages/local-explorer-ui/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,114 @@
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";
import type { FileRouteTypes } from "../routeTree.gen";
import type { FC } from "react";

interface SidebarProps {
namespaces: WorkersKvNamespace[];
loading: boolean;
interface SidebarItemGroupProps {
emptyLabel: string;
error: string | null;
icon: FC<{ className?: string }>;
items: Array<{
id: string;
isActive: boolean;
label: string;
link: {
params: object;
search?: object;
to: FileRouteTypes["to"];
};
}>;
loading: boolean;
title: string;
}

function SidebarItemGroup({
emptyLabel,
error,
icon: Icon,
items,
loading,
title,
}: SidebarItemGroupProps): JSX.Element {
return (
<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" />
<Icon className="w-3.5 h-3.5" />
{title}
</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>
) : null}

{error ? (
<li className="block py-2.5 px-4 text-danger border-b border-border">
{error}
</li>
) : null}

{!loading && !error
? items.map((item) => (
<li key={item.id}>
<Link
className={cn(
"block py-2.5 px-4 text-text no-underline border-b border-border cursor-pointer transition-colors hover:bg-border",
{
"bg-primary/8 text-primary border-l-3 border-l-primary pl-3.25":
item.isActive,
}
)}
params={item.link.params}
search={item.link.search}
to={item.link.to}
>
{item.label}
</Link>
</li>
))
: null}

{!loading && !error && items.length === 0 && (
<li className="block py-2.5 px-4 text-text-secondary border-b border-border">
{emptyLabel}
</li>
)}
</ul>
</Collapsible.Panel>
</Collapsible.Root>
);
}

interface SidebarProps {
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,58 +117,45 @@ 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" />
<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">
<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 && (
<li className="block py-2.5 px-4 text-danger border-b border-border">
{error}
</li>
)}
{!loading &&
!error &&
namespaces.map((ns) => {
const isActive = currentPath === `/kv/${ns.id}`;
return (
<li key={ns.id}>
<Link
to="/kv/$namespaceId"
params={{ namespaceId: ns.id }}
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]"
: ""
)}
>
{ns.title}
</Link>
</li>
);
})}
{!loading && !error && 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>
<SidebarItemGroup
emptyLabel="No namespaces"
error={kvError}
icon={KVIcon}
items={namespaces.map((ns) => ({
id: ns.id,
isActive: currentPath === `/kv/${ns.id}`,
label: ns.title,
link: {
params: { namespaceId: ns.id },
to: "/kv/$namespaceId",
},
}))}
loading={loading}
title="KV Namespaces"
/>

<SidebarItemGroup
emptyLabel="No databases"
error={d1Error}
icon={DatabaseIcon}
items={databases.map((db) => ({
id: db.uuid as string,
isActive: currentPath === `/d1/${db.uuid}`,
label: db.name as string,
link: {
params: { databaseId: db.uuid },
search: { table: undefined },
to: "/d1/$databaseId",
},
}))}
loading={loading}
title="D1 Databases"
/>
</aside>
);
}
Loading
Loading