Skip to content

Commit 4c6075e

Browse files
committed
add span filtering
1 parent d0f1554 commit 4c6075e

3 files changed

Lines changed: 133 additions & 15 deletions

File tree

src/index.tsx

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { MetricsView, MetricDetailsPanel } from "./metricsView";
1010
import { ClientDropdown } from "./clientDropdown";
1111
import { ResizableBox } from "./resizableBox";
1212
import { CommandPalette } from "./commandPalette";
13+
import { SpanFilterInput } from "./spanFilterInput";
1314
import type { ScrollBoxRenderable } from "@opentui/core";
1415

1516
/**
@@ -61,6 +62,7 @@ Clients Section:
6162
Navigate to select span with arrows or hjkl
6263
Press l/→ to expand and navigate into children
6364
Press h/← to collapse and navigate to parent
65+
Press "/" to open span filter (search by name)
6466
Selected span details shown in right panel
6567
6668
Metrics Section:
@@ -125,18 +127,7 @@ function AppContent() {
125127

126128
// Setup keyboard handlers
127129
useKeyboard((key) => {
128-
// Quit handlers
129-
if (key.name === "q" && !key.ctrl) {
130-
renderer.stop();
131-
process.exit(0);
132-
}
133-
if (key.raw === "\u0003") {
134-
// Ctrl+C
135-
renderer.stop();
136-
process.exit(0);
137-
}
138-
139-
// Command palette keyboard handling (must be before other handlers)
130+
// Command palette keyboard handling (must be before quit handlers)
140131
if (store.ui.showCommandPalette) {
141132
// Close on Esc
142133
if (key.name === "escape") {
@@ -176,6 +167,50 @@ function AppContent() {
176167
return;
177168
}
178169

170+
// Span filter keyboard handling (must be before quit handlers)
171+
if (store.ui.showSpanFilter) {
172+
if (key.name === "escape") {
173+
// Cancel: close filter and clear query
174+
actions.clearSpanFilter();
175+
actions.toggleSpanFilter();
176+
return;
177+
}
178+
179+
if (key.name === "return" || key.name === "enter") {
180+
// Confirm: close filter but keep query active
181+
actions.toggleSpanFilter();
182+
return;
183+
}
184+
185+
if (key.name === "backspace") {
186+
const currentQuery = store.ui.spanFilterQuery;
187+
if (currentQuery.length > 0) {
188+
actions.setSpanFilterQuery(currentQuery.slice(0, -1));
189+
}
190+
return;
191+
}
192+
193+
// Handle printable characters (including 'q')
194+
if (key.raw && key.raw.length === 1 && !key.ctrl && !key.meta) {
195+
actions.setSpanFilterQuery(store.ui.spanFilterQuery + key.raw);
196+
return;
197+
}
198+
199+
// Prevent other keys from doing anything when filter is open
200+
return;
201+
}
202+
203+
// Quit handlers (after modal input handlers)
204+
if (key.name === "q" && !key.ctrl) {
205+
renderer.stop();
206+
process.exit(0);
207+
}
208+
if (key.raw === "\u0003") {
209+
// Ctrl+C
210+
renderer.stop();
211+
process.exit(0);
212+
}
213+
179214
// Help toggle
180215
if (key.name === "?") {
181216
actions.toggleHelp();
@@ -259,6 +294,28 @@ function AppContent() {
259294
}
260295
return;
261296
}
297+
298+
// Span filter toggle with "/" key (only when spans section is focused)
299+
if (
300+
key.raw === "/" &&
301+
!key.ctrl &&
302+
!key.meta &&
303+
store.ui.focusedSection === "spans"
304+
) {
305+
actions.toggleSpanFilter();
306+
return;
307+
}
308+
309+
// Clear active span filter with Escape when not in filter input mode
310+
if (
311+
key.name === "escape" &&
312+
store.ui.focusedSection === "spans" &&
313+
store.ui.spanFilterQuery.length > 0 &&
314+
!store.ui.showSpanFilter
315+
) {
316+
actions.clearSpanFilter();
317+
return;
318+
}
262319
});
263320

264321
const statusText = () => {
@@ -356,6 +413,31 @@ function AppContent() {
356413
.pipe(Option.getOrElse(() => "None"))}`}
357414
</text>
358415

416+
{/* Span filter input (shown when typing) */}
417+
<Show when={store.ui.showSpanFilter}>
418+
<SpanFilterInput />
419+
</Show>
420+
421+
{/* Active filter indicator (shown when filter closed but query active) */}
422+
<Show
423+
when={
424+
!store.ui.showSpanFilter &&
425+
store.ui.spanFilterQuery.length > 0
426+
}
427+
>
428+
<box
429+
flexDirection="row"
430+
width="100%"
431+
paddingLeft={1}
432+
paddingBottom={1}
433+
flexShrink={0}
434+
>
435+
<text style={{ fg: "#f7768e" }}>
436+
{`Filter: "${store.ui.spanFilterQuery}" (press / to edit, Esc to clear)`}
437+
</text>
438+
</box>
439+
</Show>
440+
359441
{/* Side-by-side: Span list and details */}
360442
<box flexDirection="row" flexGrow={1}>
361443
{/* Span list - left side */}
@@ -368,6 +450,7 @@ function AppContent() {
368450
spans={store.spans}
369451
selectedSpanId={store.ui.selectedSpanId}
370452
expandedSpanIds={store.ui.expandedSpanIds}
453+
filterQuery={store.ui.spanFilterQuery || undefined}
371454
/>
372455
</scrollbox>
373456

src/spanTree.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface TreeNode {
3939
function buildHierarchicalSpanTree(
4040
spans: ReadonlyArray<SimpleSpan>,
4141
expandedSpanIds: Set<string>,
42+
filterQuery?: string,
4243
): TreeNode[] {
4344
const result: TreeNode[] = [];
4445

@@ -51,7 +52,15 @@ function buildHierarchicalSpanTree(
5152
deduped.set(span.spanId, span);
5253
}
5354
}
54-
const uniqueSpans = Array.from(deduped.values());
55+
let uniqueSpans = Array.from(deduped.values());
56+
57+
// Apply filter if provided
58+
if (filterQuery && filterQuery.trim()) {
59+
const lowerQuery = filterQuery.toLowerCase();
60+
uniqueSpans = uniqueSpans.filter((span) =>
61+
span.name.toLowerCase().includes(lowerQuery),
62+
);
63+
}
5564

5665
const spanMap = new Map(uniqueSpans.map((s) => [s.spanId, s]));
5766
const visited = new Set<string>(); // Track visited spans to prevent duplicates
@@ -226,10 +235,15 @@ export function SpanTreeView(props: {
226235
spans: ReadonlyArray<SimpleSpan>;
227236
selectedSpanId: string | null;
228237
expandedSpanIds: Set<string>;
238+
filterQuery?: string;
229239
}) {
230-
// Memoize tree - will re-run whenever spans or expandedSpanIds change
240+
// Memoize tree - will re-run whenever spans, expandedSpanIds, or filterQuery change
231241
const visibleNodes = createMemo(() =>
232-
buildHierarchicalSpanTree(props.spans, props.expandedSpanIds),
242+
buildHierarchicalSpanTree(
243+
props.spans,
244+
props.expandedSpanIds,
245+
props.filterQuery,
246+
),
233247
);
234248

235249
return (

src/store.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export interface UIState {
6767
clientsExpanded: boolean; // For client dropdown
6868
spansHeight: number; // Height of the spans section (for resizing)
6969
metricsHeight: number; // Height of the metrics section
70+
spanFilterQuery: string; // Filter query for spans
71+
showSpanFilter: boolean; // Whether span filter input is visible
7072
}
7173

7274
export interface StoreState {
@@ -120,6 +122,9 @@ export interface StoreActions {
120122
toggleExpand: () => void;
121123
setSpansHeight: (height: number) => void;
122124
setMetricsHeight: (height: number) => void;
125+
setSpanFilterQuery: (query: string) => void;
126+
toggleSpanFilter: () => void;
127+
clearSpanFilter: () => void;
123128
}
124129

125130
export interface StoreContext {
@@ -279,6 +284,8 @@ export function StoreProvider(props: ParentProps) {
279284
clientsExpanded: false,
280285
spansHeight: 35, // Default height for spans section
281286
metricsHeight: 6,
287+
spanFilterQuery: "",
288+
showSpanFilter: false,
282289
},
283290
debugCounter: 0,
284291
});
@@ -861,6 +868,20 @@ export function StoreProvider(props: ParentProps) {
861868
setMetricsHeight: (height: number) => {
862869
setStore("ui", "metricsHeight", height);
863870
},
871+
872+
setSpanFilterQuery: (query: string) => {
873+
setStore("ui", "spanFilterQuery", query);
874+
},
875+
876+
toggleSpanFilter: () => {
877+
// Just toggle visibility - don't clear query
878+
// Query is preserved so user can continue editing when reopening
879+
setStore("ui", "showSpanFilter", (prev) => !prev);
880+
},
881+
882+
clearSpanFilter: () => {
883+
setStore("ui", "spanFilterQuery", "");
884+
},
864885
};
865886

866887
// Export actions globally so Effect runtime can access them

0 commit comments

Comments
 (0)