From c4251d201d13ae2f08d8ea5397ab6dfb6947466a Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Tue, 23 Jun 2026 15:30:03 +0000 Subject: [PATCH 1/3] feat(ui): add filter bars to Errors and Logs tabs Adds per-tab filtering on the top-level Errors and Logs views: - Errors: search by message/exception type, filter by level and exception type. - Logs: search by message body, filter by severity and logger name. Filtering is gated to the standalone tabs and left out of the in-trace error/log lists. The filter bar UI mirrors the existing trace filter, extracted into a shared ListFilter component. Fixes #1315 --- .../telemetry/components/events/EventList.tsx | 105 +++++++++------ .../ui/telemetry/components/log/LogsList.tsx | 41 +++++- .../components/shared/ListFilter.tsx | 123 ++++++++++++++++++ .../src/ui/telemetry/hooks/filterTypes.ts | 16 +++ .../hooks/useErrorFiltering.test.tsx | 63 +++++++++ .../ui/telemetry/hooks/useErrorFiltering.tsx | 101 ++++++++++++++ .../telemetry/hooks/useLogsFiltering.test.tsx | 57 ++++++++ .../ui/telemetry/hooks/useLogsFiltering.tsx | 92 +++++++++++++ packages/spotlight/src/ui/telemetry/types.ts | 1 + 9 files changed, 558 insertions(+), 41 deletions(-) create mode 100644 packages/spotlight/src/ui/telemetry/components/shared/ListFilter.tsx create mode 100644 packages/spotlight/src/ui/telemetry/hooks/filterTypes.ts create mode 100644 packages/spotlight/src/ui/telemetry/hooks/useErrorFiltering.test.tsx create mode 100644 packages/spotlight/src/ui/telemetry/hooks/useErrorFiltering.tsx create mode 100644 packages/spotlight/src/ui/telemetry/hooks/useLogsFiltering.test.tsx create mode 100644 packages/spotlight/src/ui/telemetry/hooks/useLogsFiltering.tsx diff --git a/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx b/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx index 6ce112689..2421acb60 100644 --- a/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx +++ b/packages/spotlight/src/ui/telemetry/components/events/EventList.tsx @@ -1,55 +1,86 @@ import CardList from "@spotlight/ui/telemetry/components/shared/CardList"; import { OriginBadge } from "@spotlight/ui/telemetry/components/shared/OriginBadge"; import TimeSince from "@spotlight/ui/telemetry/components/shared/TimeSince"; +import { useState } from "react"; import { Link } from "react-router-dom"; import { useSentryEvents } from "../../data/useSentryEvents"; +import useErrorFiltering from "../../hooks/useErrorFiltering"; import { isErrorEvent } from "../../utils/sentry"; import { truncateId } from "../../utils/text"; import EmptyState from "../shared/EmptyState"; +import ListFilter from "../shared/ListFilter"; import PlatformIcon from "../shared/PlatformIcon"; import { EventSummary } from "./Event"; export default function EventList({ traceId }: { traceId?: string }) { const events = useSentryEvents(traceId); + const [searchQuery, setSearchQuery] = useState(""); + const [activeFilters, setActiveFilters] = useState([]); - const matchingEvents = events.filter(isErrorEvent); + const errorEvents = events.filter(isErrorEvent); + const { ERROR_FILTER_CONFIGS, filteredEvents } = useErrorFiltering(errorEvents, activeFilters, searchQuery); - return matchingEvents.length !== 0 ? ( - - {matchingEvents.map(e => { - return ( - - -
-
-
{truncateId(e.event_id)}
- + // Filtering is only exposed on the top-level Errors tab, not inside a trace's error list. + const showFilter = !traceId && errorEvents.length > 0; + const matchingEvents = traceId ? errorEvents : filteredEvents; + + const list = + matchingEvents.length !== 0 ? ( + + {matchingEvents.map(e => { + return ( + + +
+
+
{truncateId(e.event_id)}
+ +
+ + +
+
+
- - -
-
- -
- - ); - })} - - ) : ( - + + ); + })} + + ) : ( + + ); + + if (!showFilter) { + return list; + } + + return ( +
+ +
{list}
+
); } diff --git a/packages/spotlight/src/ui/telemetry/components/log/LogsList.tsx b/packages/spotlight/src/ui/telemetry/components/log/LogsList.tsx index e9ccf1929..81a407083 100644 --- a/packages/spotlight/src/ui/telemetry/components/log/LogsList.tsx +++ b/packages/spotlight/src/ui/telemetry/components/log/LogsList.tsx @@ -13,16 +13,18 @@ import { DropdownMenuTrigger, } from "@spotlight/ui/ui/dropdownMenu"; import Table from "@spotlight/ui/ui/table"; -import { type KeyboardEvent, useMemo } from "react"; +import { type KeyboardEvent, useMemo, useState } from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { LOGS_HEADERS, LOGS_SORT_KEYS, LOG_LEVEL_COLORS } from "../../constants"; import { useSentryLogs } from "../../data/useSentryLogs"; import useColumnVisibility from "../../hooks/useColumnVisibility"; +import useLogsFiltering from "../../hooks/useLogsFiltering"; import useSort from "../../hooks/useSort"; import type { SentryLogEventItem } from "../../types"; import { formatTimestamp } from "../../utils/duration"; import AnsiText from "../shared/AnsiText"; import EmptyState from "../shared/EmptyState"; +import ListFilter from "../shared/ListFilter"; import LogDetails from "./LogDetail"; type LogsComparator = (a: SentryLogEventItem, b: SentryLogEventItem) => number; @@ -50,16 +52,24 @@ const LogsList = ({ traceId }: { traceId?: string }) => { const { id: selectedLogId } = useParams(); const navigate = useNavigate(); const allLogs = useSentryLogs(traceId); + const [searchQuery, setSearchQuery] = useState(""); + const [activeFilters, setActiveFilters] = useState([]); const { sort, toggleSortOrder } = useSort({ defaultSortType: LOGS_SORT_KEYS.timestamp }); const { isColumnVisible, toggleColumn } = useColumnVisibility(LOGS_HEADERS.map(h => h.id)); + const { LOGS_FILTER_CONFIGS, filteredLogs } = useLogsFiltering(allLogs, activeFilters, searchQuery); + + // Filtering is only exposed on the top-level Logs tab, not inside a trace's log list. + const showFilter = !traceId && allLogs.length > 0; + const logs = traceId ? allLogs : filteredLogs; + const logsData = useMemo(() => { const compareLogData = COMPARATORS[sort.active] || COMPARATORS[LOGS_SORT_KEYS.timestamp]; - return allLogs.sort((a, b) => { + return [...logs].sort((a, b) => { return sort.asc ? compareLogData(a, b) : compareLogData(b, a); }); - }, [allLogs, sort.active, sort.asc]); + }, [logs, sort.active, sort.asc]); const handleRowClick = (log: SentryLogEventItem) => { navigate(`/telemetry/logs/${log.id}`); @@ -73,7 +83,8 @@ const LogsList = ({ traceId }: { traceId?: string }) => { const visibleHeaders = LOGS_HEADERS.filter(header => isColumnVisible(header.id)); - if (logsData.length === 0) { + // No logs at all (ignoring filters): show the docs-linked empty state. + if (allLogs.length === 0) { return ( { ); } + const filterBar = showFilter ? ( + + ) : null; + + // Logs exist but the current filters exclude all of them. + if (logsData.length === 0) { + return ( + + {filterBar} + + + ); + } + return ( + {filterBar}
diff --git a/packages/spotlight/src/ui/telemetry/components/shared/ListFilter.tsx b/packages/spotlight/src/ui/telemetry/components/shared/ListFilter.tsx new file mode 100644 index 000000000..3ce807e6a --- /dev/null +++ b/packages/spotlight/src/ui/telemetry/components/shared/ListFilter.tsx @@ -0,0 +1,123 @@ +import { ReactComponent as X } from "@spotlight/ui/assets/cross.svg"; +import { ReactComponent as Search } from "@spotlight/ui/assets/search.svg"; + +import { Badge } from "@spotlight/ui/ui/badge"; +import { Button } from "@spotlight/ui/ui/button"; +import { Input } from "@spotlight/ui/ui/input"; +import { useCallback, useMemo } from "react"; +import type { FilterConfigs } from "../../hooks/filterTypes"; +import { FilterDropdown } from "./FilterDropdown"; + +interface ListFilterProps { + searchQuery: string; + setSearchQuery: (query: string) => void; + activeFilters: string[]; + setActiveFilters: React.Dispatch>; + filterConfigs: FilterConfigs; + searchPlaceholder?: string; +} + +export default function ListFilter({ + searchQuery, + setSearchQuery, + activeFilters, + setActiveFilters, + filterConfigs, + searchPlaceholder = "Search...", +}: ListFilterProps) { + const handleFilterChange = useCallback( + (value: string, checked: boolean, type: "checkbox" | "radio") => { + if (type === "checkbox") { + if (checked) { + setActiveFilters(prev => [...prev, value]); + } else { + setActiveFilters(prev => prev.filter(f => f !== value)); + } + } else if (type === "radio") { + if (checked) { + setActiveFilters([value]); + } else { + setActiveFilters([]); + } + } + }, + [setActiveFilters], + ); + + const clearAllFilters = useCallback(() => { + setActiveFilters([]); + setSearchQuery(""); + }, [setSearchQuery, setActiveFilters]); + + const visibleFilterConfigs = useMemo( + () => Object.entries(filterConfigs).filter(([, config]) => config.show), + [filterConfigs], + ); + + return ( +
+
+
+ + setSearchQuery(e.target.value)} + className="border-primary-700 bg-primary-950 w-full pl-9 text-white placeholder:text-gray-400" + /> + {searchQuery && ( + + )} +
+ +
+ {visibleFilterConfigs.map(([key, config]) => ( + handleFilterChange(value, checked, config.type)} + /> + ))} +
+
+ + {activeFilters.length > 0 && ( +
+ {activeFilters.map(filter => ( + + {filter} + + + ))} + +
+ )} +
+ ); +} diff --git a/packages/spotlight/src/ui/telemetry/hooks/filterTypes.ts b/packages/spotlight/src/ui/telemetry/hooks/filterTypes.ts new file mode 100644 index 000000000..1a60cf216 --- /dev/null +++ b/packages/spotlight/src/ui/telemetry/hooks/filterTypes.ts @@ -0,0 +1,16 @@ +import type { ElementType } from "react"; + +export interface FilterOption { + label: string; + value: string; +} + +export interface FilterConfig { + icon: ElementType; + label: string; + options: FilterOption[]; + show: boolean; + type: "checkbox" | "radio"; +} + +export type FilterConfigs = Record; diff --git a/packages/spotlight/src/ui/telemetry/hooks/useErrorFiltering.test.tsx b/packages/spotlight/src/ui/telemetry/hooks/useErrorFiltering.test.tsx new file mode 100644 index 000000000..b51c002ec --- /dev/null +++ b/packages/spotlight/src/ui/telemetry/hooks/useErrorFiltering.test.tsx @@ -0,0 +1,63 @@ +import { renderHook } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import type { SentryErrorEvent } from "../types"; +import useErrorFiltering from "./useErrorFiltering"; + +function makeError(overrides: Partial & { type: string; value: string }): SentryErrorEvent { + const { type, value, ...rest } = overrides; + return { + event_id: `${type}-${value}`, + timestamp: 0, + exception: { values: [{ type, value }], value: undefined }, + ...rest, + } as SentryErrorEvent; +} + +const events: SentryErrorEvent[] = [ + makeError({ type: "TypeError", value: "cannot read x", level: "error" }), + makeError({ type: "RangeError", value: "index out of bounds", level: "warning" }), + makeError({ type: "TypeError", value: "undefined is not a function", level: "fatal" }), +]; + +describe("useErrorFiltering", () => { + it("returns all events when there is no query or filter", () => { + const { result } = renderHook(() => useErrorFiltering(events, [], "")); + expect(result.current.filteredEvents).toHaveLength(3); + }); + + it("builds level and exception type options from the events", () => { + const { result } = renderHook(() => useErrorFiltering(events, [], "")); + const { ERROR_FILTER_CONFIGS } = result.current; + expect(ERROR_FILTER_CONFIGS.level.options.map(o => o.value).sort()).toEqual(["error", "fatal", "warning"]); + expect(ERROR_FILTER_CONFIGS.type.options.map(o => o.value).sort()).toEqual(["RangeError", "TypeError"]); + }); + + it("filters by exception type", () => { + const { result } = renderHook(() => useErrorFiltering(events, ["TypeError"], "")); + expect(result.current.filteredEvents).toHaveLength(2); + }); + + it("filters by level", () => { + const { result } = renderHook(() => useErrorFiltering(events, ["fatal"], "")); + expect(result.current.filteredEvents.map(e => e.event_id)).toEqual(["TypeError-undefined is not a function"]); + }); + + it("matches the search query against exception type and value", () => { + const { result } = renderHook(() => useErrorFiltering(events, [], "out of bounds")); + expect(result.current.filteredEvents.map(e => e.event_id)).toEqual(["RangeError-index out of bounds"]); + }); + + it("combines level filter and search query", () => { + const { result } = renderHook(() => useErrorFiltering(events, ["error"], "cannot")); + expect(result.current.filteredEvents).toHaveLength(1); + const { result: noMatch } = renderHook(() => useErrorFiltering(events, ["error"], "out of bounds")); + expect(noMatch.current.filteredEvents).toHaveLength(0); + }); + + it("hides filter configs when there are no options", () => { + const withoutLevel = [makeError({ type: "Error", value: "boom" })]; + const { result } = renderHook(() => useErrorFiltering(withoutLevel, [], "")); + expect(result.current.ERROR_FILTER_CONFIGS.level.show).toBe(false); + expect(result.current.ERROR_FILTER_CONFIGS.type.show).toBe(true); + }); +}); diff --git a/packages/spotlight/src/ui/telemetry/hooks/useErrorFiltering.tsx b/packages/spotlight/src/ui/telemetry/hooks/useErrorFiltering.tsx new file mode 100644 index 000000000..0a05c5b2b --- /dev/null +++ b/packages/spotlight/src/ui/telemetry/hooks/useErrorFiltering.tsx @@ -0,0 +1,101 @@ +import { ReactComponent as AlertCircle } from "@spotlight/ui/assets/alertCircle.svg"; +import { ReactComponent as Hash } from "@spotlight/ui/assets/hash.svg"; +import { useMemo } from "react"; +import type { EventException, SentryErrorEvent } from "../types"; +import type { FilterConfigs, FilterOption } from "./filterTypes"; + +const FILTER_TYPES = { + LEVEL: "level", + TYPE: "type", +} as const; + +function exceptionValues(exception: EventException) { + return exception.value ? [exception.value] : exception.values; +} + +function getEventMessage(event: SentryErrorEvent): string { + if (typeof event.message === "string") return event.message; + if (event.message && typeof event.message.formatted === "string") return event.message.formatted; + return ""; +} + +const createFilterOptions = (items: Set): FilterOption[] => + Array.from(items).map(item => ({ label: item, value: item })); + +const useErrorFiltering = (events: SentryErrorEvent[], activeFilters: string[], searchQuery: string) => { + const { levelOptions, typeOptions } = useMemo(() => { + const levels = new Set(); + const types = new Set(); + + for (const event of events) { + if (event.level) levels.add(event.level); + for (const value of exceptionValues(event.exception)) { + if (value.type) types.add(value.type); + } + } + + return { + levelOptions: createFilterOptions(levels), + typeOptions: createFilterOptions(types), + }; + }, [events]); + + const ERROR_FILTER_CONFIGS: FilterConfigs = useMemo( + () => ({ + [FILTER_TYPES.LEVEL]: { + icon: AlertCircle, + label: "Level", + options: levelOptions, + show: levelOptions.length > 0, + type: "checkbox", + }, + [FILTER_TYPES.TYPE]: { + icon: Hash, + label: "Exception Type", + options: typeOptions, + show: typeOptions.length > 0, + type: "checkbox", + }, + }), + [levelOptions, typeOptions], + ); + + const filteredEvents = useMemo(() => { + const normalizedQuery = searchQuery.trim().toLowerCase(); + const hasQuery = Boolean(normalizedQuery); + const hasFilters = activeFilters.length > 0; + + if (!hasQuery && !hasFilters) return events; + + const levelValues = new Set(levelOptions.map(o => o.value)); + const typeValues = new Set(typeOptions.map(o => o.value)); + const activeLevels = activeFilters.filter(f => levelValues.has(f)); + const activeTypes = activeFilters.filter(f => typeValues.has(f)); + + return events.filter(event => { + if (activeLevels.length > 0 && (!event.level || !activeLevels.includes(event.level))) { + return false; + } + + const values = exceptionValues(event.exception); + + if (activeTypes.length > 0 && !values.some(v => activeTypes.includes(v.type))) { + return false; + } + + if (hasQuery) { + const haystack = [getEventMessage(event), ...values.map(v => `${v.type} ${v.value}`)].join(" ").toLowerCase(); + if (!haystack.includes(normalizedQuery)) return false; + } + + return true; + }); + }, [events, activeFilters, searchQuery, levelOptions, typeOptions]); + + return { + ERROR_FILTER_CONFIGS, + filteredEvents, + }; +}; + +export default useErrorFiltering; diff --git a/packages/spotlight/src/ui/telemetry/hooks/useLogsFiltering.test.tsx b/packages/spotlight/src/ui/telemetry/hooks/useLogsFiltering.test.tsx new file mode 100644 index 000000000..82138f877 --- /dev/null +++ b/packages/spotlight/src/ui/telemetry/hooks/useLogsFiltering.test.tsx @@ -0,0 +1,57 @@ +import { renderHook } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import type { SentryLogEventItem } from "../types"; +import useLogsFiltering from "./useLogsFiltering"; + +function makeLog(level: string, body: string, logger?: string): SentryLogEventItem { + return { + id: `${level}-${body}`, + timestamp: 0, + severity_number: 0, + sdk: undefined, + level, + body, + attributes: logger ? { "sentry.logger.name": { value: logger, type: "string" } } : undefined, + } as unknown as SentryLogEventItem; +} + +const logs: SentryLogEventItem[] = [ + makeLog("info", "user logged in", "auth"), + makeLog("error", "database connection failed", "db"), + makeLog("warn", "slow query detected", "db"), +]; + +describe("useLogsFiltering", () => { + it("returns all logs with no query or filter", () => { + const { result } = renderHook(() => useLogsFiltering(logs, [], "")); + expect(result.current.filteredLogs).toHaveLength(3); + }); + + it("builds severity and logger options", () => { + const { result } = renderHook(() => useLogsFiltering(logs, [], "")); + const { LOGS_FILTER_CONFIGS } = result.current; + expect(LOGS_FILTER_CONFIGS.level.options.map(o => o.value).sort()).toEqual(["error", "info", "warn"]); + expect(LOGS_FILTER_CONFIGS.logger.options.map(o => o.value).sort()).toEqual(["auth", "db"]); + }); + + it("filters by severity level", () => { + const { result } = renderHook(() => useLogsFiltering(logs, ["error"], "")); + expect(result.current.filteredLogs.map(l => l.body)).toEqual(["database connection failed"]); + }); + + it("filters by logger name", () => { + const { result } = renderHook(() => useLogsFiltering(logs, ["db"], "")); + expect(result.current.filteredLogs).toHaveLength(2); + }); + + it("matches the search query against the log body", () => { + const { result } = renderHook(() => useLogsFiltering(logs, [], "query")); + expect(result.current.filteredLogs.map(l => l.body)).toEqual(["slow query detected"]); + }); + + it("hides the logger filter when no logs carry a logger name", () => { + const { result } = renderHook(() => useLogsFiltering([makeLog("info", "hi")], [], "")); + expect(result.current.LOGS_FILTER_CONFIGS.logger.show).toBe(false); + expect(result.current.LOGS_FILTER_CONFIGS.level.show).toBe(true); + }); +}); diff --git a/packages/spotlight/src/ui/telemetry/hooks/useLogsFiltering.tsx b/packages/spotlight/src/ui/telemetry/hooks/useLogsFiltering.tsx new file mode 100644 index 000000000..87794edea --- /dev/null +++ b/packages/spotlight/src/ui/telemetry/hooks/useLogsFiltering.tsx @@ -0,0 +1,92 @@ +import { ReactComponent as AlertCircle } from "@spotlight/ui/assets/alertCircle.svg"; +import { ReactComponent as Hash } from "@spotlight/ui/assets/hash.svg"; +import { useMemo } from "react"; +import type { SentryLogEventItem } from "../types"; +import type { FilterConfigs, FilterOption } from "./filterTypes"; + +const FILTER_TYPES = { + LEVEL: "level", + LOGGER: "logger", +} as const; + +function getLoggerName(log: SentryLogEventItem): string | undefined { + return log.attributes?.["sentry.logger.name"]?.value as string | undefined; +} + +const createFilterOptions = (items: Set): FilterOption[] => + Array.from(items).map(item => ({ label: item, value: item })); + +const useLogsFiltering = (logs: SentryLogEventItem[], activeFilters: string[], searchQuery: string) => { + const { levelOptions, loggerOptions } = useMemo(() => { + const levels = new Set(); + const loggers = new Set(); + + for (const log of logs) { + if (log.level) levels.add(log.level); + const logger = getLoggerName(log); + if (logger) loggers.add(logger); + } + + return { + levelOptions: createFilterOptions(levels), + loggerOptions: createFilterOptions(loggers), + }; + }, [logs]); + + const LOGS_FILTER_CONFIGS: FilterConfigs = useMemo( + () => ({ + [FILTER_TYPES.LEVEL]: { + icon: AlertCircle, + label: "Severity", + options: levelOptions, + show: levelOptions.length > 0, + type: "checkbox", + }, + [FILTER_TYPES.LOGGER]: { + icon: Hash, + label: "Logger", + options: loggerOptions, + show: loggerOptions.length > 0, + type: "checkbox", + }, + }), + [levelOptions, loggerOptions], + ); + + const filteredLogs = useMemo(() => { + const normalizedQuery = searchQuery.trim().toLowerCase(); + const hasQuery = Boolean(normalizedQuery); + const hasFilters = activeFilters.length > 0; + + if (!hasQuery && !hasFilters) return logs; + + const levelValues = new Set(levelOptions.map(o => o.value)); + const loggerValues = new Set(loggerOptions.map(o => o.value)); + const activeLevels = activeFilters.filter(f => levelValues.has(f)); + const activeLoggers = activeFilters.filter(f => loggerValues.has(f)); + + return logs.filter(log => { + if (activeLevels.length > 0 && !activeLevels.includes(log.level)) { + return false; + } + + if (activeLoggers.length > 0) { + const logger = getLoggerName(log); + if (!logger || !activeLoggers.includes(logger)) return false; + } + + if (hasQuery && !log.body.toLowerCase().includes(normalizedQuery)) { + return false; + } + + return true; + }); + }, [logs, activeFilters, searchQuery, levelOptions, loggerOptions]); + + return { + LOGS_FILTER_CONFIGS, + filteredLogs, + }; +}; + +export default useLogsFiltering; diff --git a/packages/spotlight/src/ui/telemetry/types.ts b/packages/spotlight/src/ui/telemetry/types.ts index 0ad878de1..5eb1672b4 100644 --- a/packages/spotlight/src/ui/telemetry/types.ts +++ b/packages/spotlight/src/ui/telemetry/types.ts @@ -113,6 +113,7 @@ export type SentryFormattedMessage = export type SentryErrorEvent = CommonEventAttrs & { type?: "error" | "event" | "message" | "default"; + level?: "fatal" | "error" | "warning" | "info" | "debug"; exception: EventException; }; From 26cadfb17f862d76d76c2d42d3c72156a010281a Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Tue, 23 Jun 2026 15:45:45 +0000 Subject: [PATCH 2/3] fix(ui): namespace filter values by dimension Active filters were a flat list of raw option strings, with each dimension inferred by membership in its option set. When the same string existed in two dimensions (e.g. a logger and a level both named "error"), selecting it in one dropdown applied it to both, forcing rows to satisfy every matching dimension. Namespace each option value as ":" so dropdowns stay independent, and resolve the human-readable label for the active-filter badges. Adds regression tests for both hooks. --- .../components/shared/ListFilter.tsx | 13 ++++++++- .../hooks/useErrorFiltering.test.tsx | 29 ++++++++++++++----- .../ui/telemetry/hooks/useErrorFiltering.tsx | 20 +++++++------ .../telemetry/hooks/useLogsFiltering.test.tsx | 22 ++++++++++---- .../ui/telemetry/hooks/useLogsFiltering.tsx | 20 +++++++------ 5 files changed, 73 insertions(+), 31 deletions(-) diff --git a/packages/spotlight/src/ui/telemetry/components/shared/ListFilter.tsx b/packages/spotlight/src/ui/telemetry/components/shared/ListFilter.tsx index 3ce807e6a..51f39fc47 100644 --- a/packages/spotlight/src/ui/telemetry/components/shared/ListFilter.tsx +++ b/packages/spotlight/src/ui/telemetry/components/shared/ListFilter.tsx @@ -54,6 +54,17 @@ export default function ListFilter({ [filterConfigs], ); + const labelForValue = useCallback( + (value: string) => { + for (const config of Object.values(filterConfigs)) { + const option = config.options.find(o => o.value === value); + if (option) return option.label; + } + return value; + }, + [filterConfigs], + ); + return (
@@ -97,7 +108,7 @@ export default function ListFilter({
{activeFilters.map(filter => ( - {filter} + {labelForValue(filter)}