Skip to content

Commit cb71403

Browse files
NuroDevpenalosa
andauthored
feat(local-explorer-ui): Add worker filtering support (#12972)
Co-authored-by: Samuel Macleod <smacleod@cloudflare.com>
1 parent 782df44 commit cb71403

File tree

16 files changed

+694
-52
lines changed

16 files changed

+694
-52
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"miniflare": minor
3+
"@cloudflare/local-explorer-ui": minor
4+
---
5+
6+
Add worker filtering to the local explorer UI
7+
8+
When multiple workers share a dev registry, all their bindings were previously shown together in a single flat list. The explorer now shows a worker selector dropdown, letting you inspect each worker's bindings independently.
9+
10+
The selected worker is reflected in the URL as a `?worker=` search param, so deep links work correctly. By default the explorer selects the worker that is hosting the dashboard itself.

packages/local-explorer-ui/src/components/Sidebar.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import D1Icon from "../assets/icons/d1.svg?react";
66
import DOIcon from "../assets/icons/durable-objects.svg?react";
77
import KVIcon from "../assets/icons/kv.svg?react";
88
import R2Icon from "../assets/icons/r2.svg?react";
9+
import { WorkerSelector, type LocalExplorerWorker } from "./WorkerSelector";
910
import type {
1011
D1DatabaseResponse,
1112
R2Bucket,
@@ -97,6 +98,9 @@ interface SidebarProps {
9798
kvNamespaces: WorkersKvNamespace[];
9899
r2Buckets: R2Bucket[];
99100
r2Error: string | null;
101+
workers: LocalExplorerWorker[];
102+
selectedWorker: string;
103+
onWorkerChange: (workerName: string) => void;
100104
}
101105

102106
export function Sidebar({
@@ -109,7 +113,16 @@ export function Sidebar({
109113
kvNamespaces,
110114
r2Buckets,
111115
r2Error,
116+
workers,
117+
selectedWorker,
118+
onWorkerChange,
112119
}: SidebarProps) {
120+
const showWorkerSelector = workers.length > 1;
121+
122+
// Only include the worker search param when there are multiple workers.
123+
// This keeps URLs clean in the common single-worker case.
124+
const workerSearch = workers.length > 1 ? { worker: selectedWorker } : {};
125+
113126
return (
114127
<aside className="flex w-sidebar flex-col border-r border-border bg-bg-secondary">
115128
<a
@@ -127,6 +140,14 @@ export function Sidebar({
127140
</div>
128141
</a>
129142

143+
{showWorkerSelector && (
144+
<WorkerSelector
145+
workers={workers}
146+
selectedWorker={selectedWorker}
147+
onWorkerChange={onWorkerChange}
148+
/>
149+
)}
150+
130151
<SidebarItemGroup
131152
emptyLabel="No namespaces"
132153
error={kvError}
@@ -137,6 +158,7 @@ export function Sidebar({
137158
label: ns.title,
138159
link: {
139160
params: { namespaceId: ns.id },
161+
search: workerSearch,
140162
to: "/kv/$namespaceId",
141163
},
142164
}))}
@@ -153,7 +175,7 @@ export function Sidebar({
153175
label: db.name as string,
154176
link: {
155177
params: { databaseId: db.uuid },
156-
search: { table: undefined },
178+
search: { table: undefined, ...workerSearch },
157179
to: "/d1/$databaseId",
158180
},
159181
}))}
@@ -174,6 +196,7 @@ export function Sidebar({
174196
label: className,
175197
link: {
176198
params: { className },
199+
search: workerSearch,
177200
to: "/do/$className",
178201
},
179202
};
@@ -195,6 +218,7 @@ export function Sidebar({
195218
label: bucketName,
196219
link: {
197220
params: { bucketName },
221+
search: workerSearch,
198222
to: "/r2/$bucketName",
199223
},
200224
};
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { Select } from "@cloudflare/kumo/primitives/select";
2+
import {
3+
CaretUpDownIcon,
4+
CheckIcon,
5+
TerminalIcon,
6+
} from "@phosphor-icons/react";
7+
import { useState } from "react";
8+
import type { LocalExplorerWorker } from "../api";
9+
10+
// Re-export the type for convenience
11+
export type { LocalExplorerWorker };
12+
13+
// Internal workers that should be hidden from users
14+
// These are infrastructure workers created by the Vite plugin and other tooling
15+
const INTERNAL_WORKER_NAMES = new Set([
16+
"__router-worker__",
17+
"__asset-worker__",
18+
"__vite_proxy_worker__",
19+
]);
20+
21+
/**
22+
* Check if a worker name is an internal worker that should be hidden
23+
*/
24+
export function isInternalWorker(workerName: string): boolean {
25+
return INTERNAL_WORKER_NAMES.has(workerName);
26+
}
27+
28+
/**
29+
* Filter out internal workers from a list of workers
30+
*/
31+
export function filterVisibleWorkers(
32+
workers: LocalExplorerWorker[]
33+
): LocalExplorerWorker[] {
34+
return workers.filter((w) => !isInternalWorker(w.name));
35+
}
36+
37+
interface WorkerSelectorProps {
38+
workers: LocalExplorerWorker[];
39+
selectedWorker: string;
40+
onWorkerChange: (workerName: string) => void;
41+
}
42+
43+
export function WorkerSelector({
44+
workers,
45+
selectedWorker,
46+
onWorkerChange,
47+
}: WorkerSelectorProps): JSX.Element {
48+
const [open, setOpen] = useState(false);
49+
50+
const handleValueChange = (value: string | null): void => {
51+
if (value === null) {
52+
return;
53+
}
54+
onWorkerChange(value);
55+
};
56+
57+
// Find the current worker that is hosting this explorer (isSelf = true)
58+
const selfWorker = workers.find((w) => w.isSelf);
59+
60+
return (
61+
<div className="px-4 py-2">
62+
<Select.Root
63+
onOpenChange={setOpen}
64+
onValueChange={handleValueChange}
65+
open={open}
66+
value={selectedWorker}
67+
>
68+
<Select.Trigger className="flex w-full cursor-pointer items-center justify-between gap-2 rounded-md border border-border bg-bg px-3 py-2 text-sm text-text transition-colors hover:bg-bg-secondary data-popup-open:border-primary">
69+
<span className="flex items-center gap-2 truncate">
70+
<TerminalIcon className="h-4 w-4 shrink-0 text-text-secondary" />
71+
<span className="truncate">{selectedWorker}</span>
72+
</span>
73+
<Select.Icon>
74+
<CaretUpDownIcon className="h-3.5 w-3.5 shrink-0 text-text-secondary" />
75+
</Select.Icon>
76+
</Select.Trigger>
77+
78+
<Select.Portal>
79+
<Select.Positioner
80+
align="start"
81+
alignItemWithTrigger={false}
82+
className="z-100"
83+
side="bottom"
84+
sideOffset={4}
85+
>
86+
<Select.Popup className="max-h-72 min-w-48 overflow-hidden rounded-lg border border-border bg-bg shadow-[0_4px_12px_rgba(0,0,0,0.15)] transition-[opacity,transform] duration-150 data-ending-style:-translate-y-1 data-ending-style:opacity-0 data-starting-style:-translate-y-1 data-starting-style:opacity-0">
87+
<Select.List className="p-1">
88+
{workers.map((worker) => {
89+
const isSelected = selectedWorker === worker.name;
90+
const Icon = isSelected ? CheckIcon : TerminalIcon;
91+
92+
return (
93+
<Select.Item
94+
className="flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-2 text-sm text-text transition-colors outline-none select-none data-highlighted:bg-bg-secondary dark:data-highlighted:bg-bg-tertiary"
95+
key={worker.name}
96+
value={worker.name}
97+
>
98+
<span className="flex w-4 items-center">
99+
<Icon
100+
className={`h-3.5 w-3.5 ${isSelected ? "" : "text-text-secondary"}`}
101+
/>
102+
</span>
103+
<Select.ItemText>
104+
<span className="flex items-center gap-2">
105+
{worker.name}
106+
{worker.isSelf && selfWorker && (
107+
<span className="rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary">
108+
current
109+
</span>
110+
)}
111+
</span>
112+
</Select.ItemText>
113+
</Select.Item>
114+
);
115+
})}
116+
</Select.List>
117+
</Select.Popup>
118+
</Select.Positioner>
119+
</Select.Portal>
120+
</Select.Root>
121+
</div>
122+
);
123+
}

0 commit comments

Comments
 (0)