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
8 changes: 8 additions & 0 deletions .changeset/eleven-wolves-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@cloudflare/local-explorer-ui": minor
"miniflare": minor
---

local explorer: fix handling on resources that are bound to multiple workers

Note the local explorer is a experimental feature still.
6 changes: 5 additions & 1 deletion fixtures/worker-with-resources/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ describe("local explorer", () => {
id: "some-kv-id",
title: "KV_WITH_ID",
},
{
id: "worker-b-kv-id",
title: "KV_B",
},
],
result_info: {
count: 2,
count: 3,
},
success: true,
});
Expand Down
6 changes: 5 additions & 1 deletion fixtures/worker-with-resources/worker-configuration.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --no-include-runtime` (hash: 3080c55aa34f3cfa38c5e93fb9275475)
// Generated by Wrangler by running `wrangler types --no-include-runtime` (hash: 5770eff0c6b7e72c420371c2704d44b3)
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./src/index");
Expand All @@ -8,10 +8,14 @@ declare namespace Cloudflare {
interface Env {
KV: KVNamespace;
KV_WITH_ID: KVNamespace;
KV_B: KVNamespace;
BUCKET: R2Bucket;
BUCKET_B: R2Bucket;
DB: D1Database;
BACKUP_DB: D1Database;
DB_B: D1Database;
DO: DurableObjectNamespace<import("./src/index").MyDurableObject>;
DO_B: DurableObjectNamespace /* WorkerBDurableObject from worker-b */;
MY_WORKFLOW: Workflow<
Parameters<import("./src/index").MyWorkflow["run"]>[0]["payload"]
>;
Expand Down
14 changes: 14 additions & 0 deletions fixtures/worker-with-resources/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"bucket_name": "my-bucket",
"binding": "BUCKET",
},
{ "bucket_name": "bucket-b", "binding": "BUCKET_B" },
],
"kv_namespaces": [
{
Expand All @@ -17,6 +18,10 @@
"binding": "KV_WITH_ID",
"id": "some-kv-id",
},
{
"binding": "KV_B",
"id": "worker-b-kv-id",
},
],
"d1_databases": [
{
Expand All @@ -26,6 +31,10 @@
"binding": "BACKUP_DB",
"database_id": "some-db-id",
},
{
"binding": "DB_B",
"database_id": "worker-b-db-id",
},
],
"workflows": [
{
Expand All @@ -40,6 +49,11 @@
"name": "DO",
"class_name": "MyDurableObject",
},
{
"name": "DO_B",
"class_name": "WorkerBDurableObject",
"script_name": "worker-b",
},
],
},
"migrations": [
Expand Down
164 changes: 62 additions & 102 deletions packages/local-explorer-ui/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,12 @@ import KVIcon from "../assets/icons/kv.svg?react";
import R2Icon from "../assets/icons/r2.svg?react";
import WorkflowsIcon from "../assets/icons/workflows.svg?react";
import { WorkerSelector, type LocalExplorerWorker } from "./WorkerSelector";
import type {
D1DatabaseResponse,
R2Bucket,
WorkersKvNamespace,
WorkersNamespace,
WorkflowsWorkflow,
} from "../api";
import type { LocalExplorerWorkerBindings } from "../api";
import type { FileRouteTypes } from "../routeTree.gen";
import type { FC } from "react";

interface SidebarItemGroupProps {
emptyLabel: string;
error: string | null;
icon: FC<{ className?: string }>;
items: Array<{
id: string;
Expand All @@ -37,7 +30,6 @@ interface SidebarItemGroupProps {

function SidebarItemGroup({
emptyLabel,
error,
icon: Icon,
items,
title,
Expand All @@ -55,32 +47,26 @@ function SidebarItemGroup({

<Collapsible.Panel className="overflow-hidden transition-[height,opacity] duration-200 ease-out data-ending-style:h-0 data-ending-style:opacity-0 data-starting-style:h-0 data-starting-style:opacity-0">
<ul className="ml-3 list-none space-y-0.5 border-l border-kumo-fill pl-3">
{error ? (
<li className="px-2 py-1.5 text-sm text-kumo-danger">{error}</li>
) : null}

{!error
? items.map((item) => (
<li key={item.id}>
<Link
className={cn(
"block cursor-pointer rounded-l-md px-2 py-2.5 text-sm text-kumo-default no-underline transition-colors hover:bg-kumo-brand/10",
{
"bg-kumo-brand/10 font-medium text-kumo-link hover:bg-kumo-brand/20":
item.isActive,
}
)}
params={item.link.params}
search={item.link.search}
to={item.link.to}
>
{item.label}
</Link>
</li>
))
: null}
{items.map((item) => (
<li key={item.id}>
<Link
className={cn(
"block cursor-pointer rounded-l-md px-2 py-2.5 text-sm text-kumo-default no-underline transition-colors hover:bg-kumo-brand/10",
{
"bg-kumo-brand/10 font-medium text-kumo-link hover:bg-kumo-brand/20":
item.isActive,
}
)}
params={item.link.params}
search={item.link.search}
to={item.link.to}
>
{item.label}
</Link>
</li>
))}

{!error && items.length === 0 && (
{items.length === 0 && (
<li className="px-2 py-1.5 text-sm text-kumo-subtle italic">
{emptyLabel}
</li>
Expand All @@ -93,43 +79,28 @@ function SidebarItemGroup({

interface SidebarProps {
currentPath: string;
d1Error: string | null;
databases: D1DatabaseResponse[];
doError: string | null;
doNamespaces: WorkersNamespace[];
kvError: string | null;
kvNamespaces: WorkersKvNamespace[];
r2Buckets: R2Bucket[];
r2Error: string | null;
bindings?: LocalExplorerWorkerBindings;
workers: LocalExplorerWorker[];
selectedWorker: string;
onWorkerChange: (workerName: string) => void;
workflows: WorkflowsWorkflow[];
workflowsError: string | null;
}

export function Sidebar({
currentPath,
d1Error,
databases,
doError,
doNamespaces,
kvError,
kvNamespaces,
r2Buckets,
r2Error,
bindings,
workers,
selectedWorker,
onWorkerChange,
workflows,
workflowsError,
}: SidebarProps) {
const showWorkerSelector = workers.length > 1;

// Only include the worker search param when there are multiple workers.
// This keeps URLs clean in the common single-worker case.
const workerSearch = workers.length > 1 ? { worker: selectedWorker } : {};

const kvNamespaces = bindings?.kv ?? [];
const databases = bindings?.d1 ?? [];
const doNamespaces = (bindings?.do ?? []).filter((ns) => ns.useSqlite);
const r2Buckets = bindings?.r2 ?? [];
const workflows = bindings?.workflows ?? [];

return (
<aside className="flex w-sidebar flex-col border-r border-kumo-fill bg-kumo-elevated">
<a
Expand Down Expand Up @@ -157,12 +128,11 @@ export function Sidebar({

<SidebarItemGroup
emptyLabel="No namespaces"
error={kvError}
icon={KVIcon}
items={kvNamespaces.map((ns) => ({
id: ns.id,
isActive: currentPath === `/kv/${ns.id}`,
label: ns.title,
label: ns.bindingName,
link: {
params: { namespaceId: ns.id },
search: workerSearch,
Expand All @@ -174,14 +144,13 @@ export function Sidebar({

<SidebarItemGroup
emptyLabel="No databases"
error={d1Error}
icon={D1Icon}
items={databases.map((db) => ({
id: db.uuid as string,
isActive: currentPath === `/d1/${db.uuid}`,
label: db.name as string,
id: db.id,
isActive: currentPath === `/d1/${db.id}`,
label: db.bindingName,
link: {
params: { databaseId: db.uuid },
params: { databaseId: db.id },
search: { table: undefined, ...workerSearch },
to: "/d1/$databaseId",
},
Expand All @@ -191,59 +160,50 @@ export function Sidebar({

<SidebarItemGroup
emptyLabel="No SQLite namespaces"
error={doError}
icon={DOIcon}
items={doNamespaces.map((ns) => {
const className = ns.class ?? ns.name ?? ns.id ?? "Unknown";
return {
id: ns.id as string,
isActive:
currentPath === `/do/${className}` ||
currentPath.startsWith(`/do/${className}/`),
label: className,
link: {
params: { className },
search: workerSearch,
to: "/do/$className",
},
};
})}
items={doNamespaces.map((ns) => ({
id: ns.id,
isActive:
currentPath === `/do/${ns.className}` ||
currentPath.startsWith(`/do/${ns.className}/`),
label: ns.className,
link: {
params: { className: ns.className },
search: workerSearch,
to: "/do/$className",
},
}))}
title="Durable Objects"
/>

<SidebarItemGroup
emptyLabel="No buckets"
error={r2Error}
icon={R2Icon}
items={r2Buckets.map((bucket) => {
const bucketName = bucket.name ?? "Unknown";
return {
id: bucketName,
isActive:
currentPath === `/r2/${bucketName}` ||
currentPath.startsWith(`/r2/${bucketName}/`),
label: bucketName,
link: {
params: { bucketName },
search: workerSearch,
to: "/r2/$bucketName",
},
};
})}
items={r2Buckets.map((bucket) => ({
id: bucket.id,
isActive:
currentPath === `/r2/${bucket.id}` ||
currentPath.startsWith(`/r2/${bucket.id}/`),
label: bucket.bindingName,
link: {
params: { bucketName: bucket.id },
search: workerSearch,
to: "/r2/$bucketName",
},
}))}
title="R2 Buckets"
/>
<SidebarItemGroup
emptyLabel="No workflows"
error={workflowsError}
icon={WorkflowsIcon}
items={workflows.map((wf) => ({
id: wf.name as string,
id: wf.id,
isActive:
currentPath === `/workflows/${wf.name}` ||
currentPath.startsWith(`/workflows/${wf.name}/`),
label: wf.name as string,
currentPath === `/workflows/${wf.id}` ||
currentPath.startsWith(`/workflows/${wf.id}/`),
label: wf.bindingName,
link: {
params: { workflowName: wf.name },
params: { workflowName: wf.id },
search: workerSearch,
to: "/workflows/$workflowName",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function TableSelect({
}

void navigate({
search: { table: tableName },
search: (prev) => ({ ...prev, table: tableName }),
to: ".",
});
},
Expand Down
18 changes: 18 additions & 0 deletions packages/local-explorer-ui/src/components/WorkerSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ export function filterVisibleWorkers(
return workers.filter((w) => !isInternalWorker(w.name));
}

/**
* Get the selected worker based on URL params and available workers
*/
export function getSelectedWorker(
workers: LocalExplorerWorker[],
searchStr: string
): LocalExplorerWorker | undefined {
const visibleWorkers = filterVisibleWorkers(workers);
const workerFromUrl = new URLSearchParams(searchStr).get("worker");
const defaultWorker =
visibleWorkers.find((w) => w.isSelf)?.name ?? visibleWorkers[0]?.name;
const selectedWorkerName =
workerFromUrl && visibleWorkers.some((w) => w.name === workerFromUrl)
? workerFromUrl
: defaultWorker;
return visibleWorkers.find((w) => w.name === selectedWorkerName);
}

interface WorkerSelectorProps {
workers: LocalExplorerWorker[];
selectedWorker: string;
Expand Down
Loading
Loading