Skip to content

Commit 7edc4b4

Browse files
breezy-devsclaude
andcommitted
web: add container filter to log pane
Adds a container filter dropdown to the action bar when a resource has logs from 2 or more distinct containers. Selection is URL-persisted via the existing search param pattern and integrates with the existing FilterSet and log display pipeline. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Brent Hiranaka <brenthiranaka95@gmail.com>
1 parent 282e70a commit 7edc4b4

12 files changed

Lines changed: 518 additions & 8 deletions

web/src/CopyLogs.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const DEFAULT_FILTER_SET: FilterSet = {
2323
source: FilterSource.all,
2424
level: FilterLevel.all,
2525
term: EMPTY_FILTER_TERM,
26+
containers: [],
2627
}
2728

2829
describe("CopyLogs", () => {

web/src/LogStore.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,24 @@ class LogStore implements LogAlertIndex {
418418
return result
419419
}
420420

421+
// Returns the distinct container names seen in log lines for the given manifest.
422+
// Only includes names from lines that carry a container field (pod logs).
423+
// Returns an empty array for resources with no multi-container log data.
424+
containersForManifest(mn: string): string[] {
425+
const manifestSpans = this.spansForManifest(mn)
426+
const seen = new Set<string>()
427+
for (const line of this.lines) {
428+
if (!manifestSpans[line.spanId]) {
429+
continue
430+
}
431+
const name = line.fields?.container
432+
if (name) {
433+
seen.add(name)
434+
}
435+
}
436+
return Array.from(seen)
437+
}
438+
421439
getOrderedBuildSpanIds(spanId: string): string[] {
422440
let startSpan = this.spans[spanId]
423441
if (!startSpan) {
@@ -630,6 +648,7 @@ class LogStore implements LogAlertIndex {
630648
level: storedLine.level,
631649
manifestName: span.manifestName,
632650
buildEvent: storedLine.fields?.buildEvent,
651+
containerName: storedLine.fields?.container,
633652
spanId: spanId,
634653
storedLineIndex: i,
635654
}

web/src/OverviewActionBar.test.tsx

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,24 @@ import { useLocation } from "react-router-dom"
1111
import { SnackbarProvider } from "notistack"
1212
import React from "react"
1313
import { ButtonSet } from "./ApiButton"
14-
import { EMPTY_FILTER_TERM, FilterLevel, FilterSource } from "./logfilters"
14+
import {
15+
EMPTY_FILTER_TERM,
16+
FilterLevel,
17+
FilterSet,
18+
FilterSource,
19+
} from "./logfilters"
1520
import OverviewActionBar, {
1621
createLogSearch,
1722
FILTER_INPUT_DEBOUNCE,
1823
} from "./OverviewActionBar"
1924
import { EmptyBar, FullBar } from "./OverviewActionBar.stories"
2025
import { disableButton, oneResource, oneUIButton } from "./testdata"
2126

22-
const DEFAULT_FILTER_SET = {
27+
const DEFAULT_FILTER_SET: FilterSet = {
2328
level: FilterLevel.all,
2429
source: FilterSource.all,
2530
term: EMPTY_FILTER_TERM,
31+
containers: [],
2632
}
2733

2834
let location: any = window.location
@@ -257,6 +263,104 @@ describe("OverviewActionBar", () => {
257263
})
258264
})
259265

266+
describe("FilterContainerMenu", () => {
267+
const containers = ["app", "istio-proxy", "daprd"]
268+
269+
it("does not render when fewer than 2 containers are present", () => {
270+
customRender(
271+
<OverviewActionBar
272+
filterSet={DEFAULT_FILTER_SET}
273+
logContainers={["app"]}
274+
/>
275+
)
276+
expect(
277+
screen.queryByLabelText(/select containers to filter logs/i)
278+
).toBeNull()
279+
})
280+
281+
it("renders when 2 or more containers are present", () => {
282+
customRender(
283+
<OverviewActionBar
284+
filterSet={DEFAULT_FILTER_SET}
285+
logContainers={["app", "istio-proxy"]}
286+
/>
287+
)
288+
expect(
289+
screen.getByLabelText(/select containers to filter logs/i)
290+
).toBeInTheDocument()
291+
})
292+
293+
it("shows the container name in the right pill when 1 container is selected", () => {
294+
customRender(
295+
<OverviewActionBar
296+
filterSet={{ ...DEFAULT_FILTER_SET, containers: ["app"] }}
297+
logContainers={containers}
298+
/>
299+
)
300+
expect(
301+
screen.getByLabelText(/select containers to filter logs/i)
302+
).toHaveTextContent("app")
303+
})
304+
305+
it("shows a count in the right pill when 2+ containers are selected", () => {
306+
customRender(
307+
<OverviewActionBar
308+
filterSet={{
309+
...DEFAULT_FILTER_SET,
310+
containers: ["app", "istio-proxy"],
311+
}}
312+
logContainers={containers}
313+
/>
314+
)
315+
expect(
316+
screen.getByLabelText(/select containers to filter logs/i)
317+
).toHaveTextContent("2")
318+
})
319+
320+
it("updates the URL when a container is toggled on", () => {
321+
customRender(
322+
<OverviewActionBar
323+
filterSet={DEFAULT_FILTER_SET}
324+
logContainers={containers}
325+
/>
326+
)
327+
userEvent.click(
328+
screen.getByLabelText(/select containers to filter logs/i)
329+
)
330+
userEvent.click(screen.getByRole("menuitemcheckbox", { name: /app/i }))
331+
expect(getSearch()).toContain("containers=")
332+
expect(new URLSearchParams(getSearch()).get("containers")).toBe("app")
333+
})
334+
335+
it("removes a container from the URL when it is toggled off", () => {
336+
customRender(
337+
<OverviewActionBar
338+
filterSet={{ ...DEFAULT_FILTER_SET, containers: ["app"] }}
339+
logContainers={containers}
340+
/>
341+
)
342+
userEvent.click(
343+
screen.getByLabelText(/select containers to filter logs/i)
344+
)
345+
userEvent.click(screen.getByRole("menuitemcheckbox", { name: /app/i }))
346+
expect(new URLSearchParams(getSearch()).get("containers")).toBeNull()
347+
})
348+
349+
it("clears the containers filter when 'All Containers' is clicked", () => {
350+
customRender(
351+
<OverviewActionBar
352+
filterSet={{ ...DEFAULT_FILTER_SET, containers: ["app"] }}
353+
logContainers={containers}
354+
/>
355+
)
356+
userEvent.click(
357+
screen.getByLabelText(/select containers to filter logs/i)
358+
)
359+
userEvent.click(screen.getByRole("menuitem", { name: /all containers/i }))
360+
expect(new URLSearchParams(getSearch()).get("containers")).toBeNull()
361+
})
362+
})
363+
260364
describe("createLogSearch", () => {
261365
let currentSearch: URLSearchParams
262366
beforeEach(() => (currentSearch = new URLSearchParams()))
@@ -311,5 +415,31 @@ describe("OverviewActionBar", () => {
311415
}).toString()
312416
).toBe("source=build&term=test")
313417
})
418+
419+
it("sets the containers param when a non-empty array is passed", () => {
420+
expect(
421+
createLogSearch(currentSearch.toString(), {
422+
containers: ["app", "istio"],
423+
}).get("containers")
424+
).toBe("app,istio")
425+
})
426+
427+
it("removes the containers param when an empty array is passed", () => {
428+
currentSearch.set("containers", "app,istio")
429+
expect(
430+
createLogSearch(currentSearch.toString(), { containers: [] }).get(
431+
"containers"
432+
)
433+
).toBeNull()
434+
})
435+
436+
it("preserves an existing containers param when containers is undefined", () => {
437+
currentSearch.set("containers", "app")
438+
expect(
439+
createLogSearch(currentSearch.toString(), { term: "test" }).get(
440+
"containers"
441+
)
442+
).toBe("app")
443+
})
314444
})
315445
})

0 commit comments

Comments
 (0)