feat: add new SVG icons and refactor ConfigProvider to use React Query for configuration fetching#17
Conversation
…y for configuration fetching - Introduced `iii-black1.1.svg` and `iii-white1.1.svg` for UI enhancements. - Refactored `ConfigProvider` to utilize `useQuery` from React Query for fetching console configuration, improving error handling and state management. - Updated various components to enhance accessibility and maintainability, including adding IDs to input elements and improving state management in filters and charts.
📝 WalkthroughWalkthroughThis PR refactors state management across the console frontend, replacing scattered useState hooks with reducer-based machines for improved organization in routes (functions, logs, states, streams, triggers, traces). It introduces interactive enhancements to trace visualization (FlameGraph with minimap, zoom, selection), accessibility improvements (labels, button types, Escape-key handling), and adjusts API integration patterns (React Query for config, safer null handling). Multiple components also export new types and update public signatures. Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/console-frontend/src/routes/states.tsx (1)
231-251:⚠️ Potential issue | 🟡 MinorBug:
handleEditItemincorrectly usesaddModalreducer for saving state.The
handleEditItemfunction dispatchesSTART_SAVEandSAVE_DONEtodispatchAddModalinstead of tracking save state for edits separately. This causes the modal'ssavingstate to change during edit operations, which could lead to incorrect UI behavior if the add modal is open during an edit (unlikely but possible), and is semantically incorrect.Consider adding a
savingfield toEditStateor creating a separate loading state.🔧 Proposed fix - extend EditState
interface EditState { editingItem: string | null editValue: string + saving: boolean } type EditAction = | { type: 'OPEN_EDIT'; itemKey: string; value: string } | { type: 'SET_EDIT_VALUE'; value: string } | { type: 'CLOSE_EDIT' } + | { type: 'START_EDIT_SAVE' } + | { type: 'EDIT_SAVE_DONE' } -const editInitial: EditState = { editingItem: null, editValue: '' } +const editInitial: EditState = { editingItem: null, editValue: '', saving: false } function editReducer(state: EditState, action: EditAction): EditState { switch (action.type) { case 'OPEN_EDIT': - return { editingItem: action.itemKey, editValue: action.value } + return { editingItem: action.itemKey, editValue: action.value, saving: false } case 'SET_EDIT_VALUE': return { ...state, editValue: action.value } case 'CLOSE_EDIT': - return { editingItem: null, editValue: '' } + return { editingItem: null, editValue: '', saving: false } + case 'START_EDIT_SAVE': + return { ...state, saving: true } + case 'EDIT_SAVE_DONE': + return { ...state, saving: false } default: return state } }Then update
handleEditItem:const handleEditItem = async (item: StateItem) => { if (!selectedGroupId) return - dispatchAddModal({ type: 'START_SAVE' }) + dispatchEdit({ type: 'START_EDIT_SAVE' }) try { // ... existing logic ... dispatchEdit({ type: 'CLOSE_EDIT' }) refetchItems() } catch { // Handle error } finally { - dispatchAddModal({ type: 'SAVE_DONE' }) + dispatchEdit({ type: 'EDIT_SAVE_DONE' }) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/routes/states.tsx` around lines 231 - 251, handleEditItem is incorrectly toggling the add-modal saving state via dispatchAddModal({ type: 'START_SAVE' }) / 'SAVE_DONE'; change it to track saving on the edit reducer instead: add a saving boolean to EditState (or a separate editLoading flag) and dispatch edit actions (e.g., dispatchEdit({ type: 'START_SAVE' }) and dispatchEdit({ type: 'SAVE_DONE' })) inside handleEditItem around the async setStateItem call, and remove the dispatchAddModal calls so the add modal saving state is not affected.packages/console-frontend/src/routes/index.tsx (1)
29-43:⚠️ Potential issue | 🟠 MajorThe route unconditionally redirects, making DashboardPage unreachable dead code.
The loader (line 41) always throws a redirect to
/functions, so theDashboardPagecomponent (lines 194–663) will never render. This creates ~470 lines of unused code. Either remove the component and related code, or make the redirect conditional if the dashboard should be accessible.Note: The
Promise.allSettled()prefetch (lines 34–40) is not awaited, so it fires asynchronously after the redirect is thrown—the prefetch likely won't complete before navigation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/routes/index.tsx` around lines 29 - 43, The loader for createFileRoute('/') always throws redirect({ to: '/functions' }) so DashboardPage is never rendered; either remove the DashboardPage component and its related code, or make the redirect conditional in the loader (e.g., check a flag/user state or query param) and only redirect when appropriate; if you keep the loader prefetch logic, await Promise.allSettled([...queryClient.prefetchQuery(statusQuery), queryClient.prefetchQuery(functionsQuery()), queryClient.prefetchQuery(triggersQuery()), queryClient.prefetchQuery(streamsQuery()), queryClient.prefetchQuery(metricsHistoryQuery(100))]) before redirecting so prefetch completes, and ensure you return appropriate loader data instead of unconditionally throwing from loader when DashboardPage must be reachable.
🧹 Nitpick comments (15)
packages/console-frontend/src/components/ui/card.tsx (1)
27-37: Good fix: CardTitle now correctly renders children.The change from a self-closing
<h3 {...props} />to explicit<h3>{children}</h3>is correct—spreading props on a self-closing tag doesn't render children in JSX.Pre-existing type mismatch: The ref type is
HTMLParagraphElementbut the element is<h3>(HTMLHeadingElement). Consider aligning the types:🔧 Suggested fix for type consistency
export const CardTitle = React.forwardRef< - HTMLParagraphElement, + HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement> >(({ className, children, ...props }, ref) => (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/components/ui/card.tsx` around lines 27 - 37, The CardTitle forwardRef has a type mismatch: the ref is declared as HTMLParagraphElement while the component renders an <h3> (a heading). Update the generics and ref type on CardTitle to use HTMLHeadingElement and React.HTMLAttributes<HTMLHeadingElement> so the ref and props match the rendered <h3> element (update the forwardRef signature and ref generic accordingly).packages/console-frontend/src/components/traces/TraceMap.tsx (1)
100-117: Defensive null check is good, but angle calculation has a subtle issue if filtering ever occurs.The refactor to
.map().filter()with type narrowing is idiomatic. However, sinceserviceNamescomes fromserviceMap.keys()(line 94),serviceMap.get(name)will always return a defined value—the null check is purely defensive.The subtle issue: angles are computed using index
iandnodeCountfrom the pre-filter array. If a node were ever filtered out, the circular layout would have gaps instead of redistributing evenly. This is currently unreachable but would become a bug if the logic ever changes.♻️ Option: recalculate angles after filtering for correctness
- const nodes: ServiceNode[] = serviceNames - .map((name, i) => { - const stats = serviceMap.get(name) - if (!stats) return null - const angle = (i / nodeCount) * 2 * Math.PI - Math.PI / 2 - - return { - id: name, - name, - spanCount: stats.spanCount, - totalDuration: stats.totalDuration, - errorCount: stats.errorCount, - x: centerX + radius * Math.cos(angle), - y: centerY + radius * Math.sin(angle), - color: getServiceColor(name), - } - }) - .filter((n): n is ServiceNode => n !== null) + const validServices = serviceNames + .map((name) => ({ name, stats: serviceMap.get(name) })) + .filter((s): s is { name: string; stats: NonNullable<typeof s.stats> } => s.stats != null) + + const nodes: ServiceNode[] = validServices.map(({ name, stats }, i) => { + const angle = (i / validServices.length) * 2 * Math.PI - Math.PI / 2 + return { + id: name, + name, + spanCount: stats.spanCount, + totalDuration: stats.totalDuration, + errorCount: stats.errorCount, + x: centerX + radius * Math.cos(angle), + y: centerY + radius * Math.sin(angle), + color: getServiceColor(name), + } + })Alternatively, since the null case is unreachable, you could simplify by using the non-null assertion (
serviceMap.get(name)!) with a comment explaining the invariant—though the current defensive approach is reasonable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/components/traces/TraceMap.tsx` around lines 100 - 117, The angle calculation for nodes uses the pre-filter index i and nodeCount which can produce gaps if any items are filtered out; update TraceMap.tsx so angles are computed after filtering: first build the filtered list of valid services (use serviceMap.get(name) to filter and produce a ServiceNode-like partial), then map that filtered array with its own index to compute angle using its length as the nodeCount, or alternatively assert non-null with serviceMap.get(name)! and remove the defensive filter (add a comment explaining the invariant). Ensure you update references to nodes, serviceNames, nodeCount, and the angle computation to use the post-filter array/length so layout evenly redistributes.packages/console-frontend/src/components/filters/TimeRangeFilter.tsx (1)
318-330: Static ID could cause duplicate IDs if multiple instances are rendered.The hardcoded
id="time-range-start"will create invalid HTML if multipleTimeRangeFiltercomponents are mounted simultaneously. Consider usingReact.useId()to generate unique IDs per instance.♻️ Suggested fix using useId
Add to imports and component body:
-import { useCallback, useMemo, useState } from 'react' +import { useCallback, useId, useMemo, useState } from 'react'Inside the component:
export function TimeRangeFilter({ ... }: TimeRangeFilterProps) { + const id = useId() const [isOpen, setIsOpen] = useState(false)Then use dynamic IDs:
- htmlFor="time-range-start" + htmlFor={`${id}-start`} ... - id="time-range-start" + id={`${id}-start`}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/components/filters/TimeRangeFilter.tsx` around lines 318 - 330, The label/input pair in the TimeRangeFilter component uses a hardcoded id ("time-range-start") which can collide when multiple instances mount; import and call React.useId() (or useId from React) inside the TimeRangeFilter component to create a unique base id (e.g., idBase = useId()) and replace the static id and htmlFor with a scoped id like `${idBase}-start` (and similarly update any matching end input if present) so the Input's id and the label's htmlFor remain unique per component instance while keeping the existing value/onChange (customStart, setCustomStart) intact.packages/console-frontend/src/api/config-provider.tsx (2)
38-44: Consider settingstaleTimeand reviewing retry behavior.The config data is unlikely to change during a session. Setting
staleTime: Infinity(or a large value) would prevent unnecessary refetches. Also,retry: trueenables infinite retries—if that's intentional, it's fine; otherwise, consider a finite number likeretry: 3.Suggested configuration
const { data: config, error } = useQuery({ queryKey: ['console-config'], queryFn: fetchConsoleConfig, retry: true, retryDelay: 3000, + staleTime: Infinity, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/api/config-provider.tsx` around lines 38 - 44, The useQuery in ConfigProvider is currently missing a staleTime and uses retry: true (infinite retries); update the query options in ConfigProvider (the useQuery call) to set staleTime: Infinity (or a large ms value) to avoid refetching session-static console config, and change retry from true to a finite count such as retry: 3 (or an appropriate number) to prevent infinite retry loops.
33-35: Side effect inside queryFn may cause unintended behavior.Calling
setConfig(data)inside the query function means it runs on every successful fetch, including refetches and retries. If the config module caches state, this is fine. However, ifsetConfighas side effects (e.g., triggering re-renders or persisting to storage), consider moving it to anonSuccesscallback pattern or ensuring idempotency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/api/config-provider.tsx` around lines 33 - 35, The query function currently calls setConfig(data) after parsing the response, causing side effects on every fetch/retry; move the state mutation out of the query function and into an onSuccess callback (or the component/caller that invokes useQuery) so the queryFn only returns the fetched ConsoleConfig and setConfig is invoked once or guarded for idempotency; specifically, remove the setConfig(data) call from the fetch logic in config-provider.tsx and instead call setConfig from the useQuery onSuccess handler (or wrap setConfig to no-op when the value is unchanged) so setConfig and any re-render/persistence side effects are controlled.packages/console-frontend/src/routes/traces.tsx (1)
284-317: Consider throttling resize updates with requestAnimationFrame.The resize handler updates state on every
mousemoveevent, which can fire many times per second and cause unnecessary re-renders. The WaterfallChart component uses RAF throttling for its resize logic.Example throttling pattern
useEffect(() => { + let rafId: number | null = null const onMouseMove = (e: MouseEvent) => { if (!isResizingRef.current) return + if (rafId !== null) return + rafId = requestAnimationFrame(() => { + rafId = null + if (!isResizingRef.current) return const dx = resizeStartRef.current.x - e.clientX // ... rest of logic + }) } const onMouseUp = () => { if (isResizingRef.current) { + if (rafId !== null) { + cancelAnimationFrame(rafId) + rafId = null + } isResizingRef.current = null // ... } } // ... + return () => { + // ... cleanup + if (rafId !== null) cancelAnimationFrame(rafId) + } }, [])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/routes/traces.tsx` around lines 284 - 317, The mousemove handler currently calls setPanelWidths on every event causing excessive re-renders; throttle updates using requestAnimationFrame by adding an RAF loop inside the useEffect: in onMouseMove (used with isResizingRef and resizeStartRef) capture the desired width calculation (using clampPanelWidth, PANEL_MIN_WIDTH, totalWidth, etc.) into a local "next" value and schedule a single requestAnimationFrame to call setPanelWidths(trace/ span) if one isn’t already pending; store the raf id in a ref so you can cancel it on cleanup and when onMouseUp clears isResizingRef and cancels any pending RAF to avoid orphaned frames.packages/console-frontend/src/components/traces/WaterfallChart.tsx (1)
270-284: Userole="separator"instead ofrole="slider"for resize handles.A resize handle is semantically a separator that can be moved, not a slider. The
aria-orientationshould also be"vertical"since dragging is horizontal but it separates content vertically.Suggested fix
<div - role="slider" + role="separator" aria-label="Resize span column" - aria-orientation="horizontal" + aria-orientation="vertical" aria-valuemin={150} aria-valuemax={600} aria-valuenow={spanColWidth}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/components/traces/WaterfallChart.tsx` around lines 270 - 284, In WaterfallChart.tsx update the resize handle element: change role="slider" to role="separator", set aria-orientation="vertical" (since it separates columns), and remove the slider-specific attributes aria-valuemin, aria-valuemax and aria-valuenow; keep tabIndex, the event handlers (startColResize, onDoubleClick that calls setSpanColWidth(300)), title and styling so keyboard/mouse accessibility is preserved.packages/console-frontend/src/routes/functions.tsx (1)
226-229: Consider including HTTP status in the result.The
InvocationResultinterface definesstatus?: number(line 30), but it's never populated when setting the result. IfinvokeFunctionApireturns a status code, consider including it for debugging purposes.If status is available from the API
dispatchInvocation({ type: 'SET_RESULT', - result: { success: result.success, duration, data: result.data, error: result.error }, + result: { success: result.success, status: result.status, duration, data: result.data, error: result.error }, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/routes/functions.tsx` around lines 226 - 229, The dispatchInvocation call that sets the invocation result is missing the HTTP status field defined on InvocationResult; update the object passed to dispatchInvocation (inside the function that calls invokeFunctionApi and currently uses dispatchInvocation({ type: 'SET_RESULT', result: { success: result.success, duration, data: result.data, error: result.error } })) to include status (e.g., status: result.status or the response HTTP status returned by invokeFunctionApi) so the SET_RESULT payload populates InvocationResult.status for better debugging.packages/console-frontend/src/routes/logs.tsx (1)
285-302: Consider resetting page to 1 when filters change.When
searchQueryoractiveLevelFilterschange, thefilteredLogsarray may shrink, butcurrentPageisn't reset. This can result in an empty page view if the user was on page 5 and the filtered results only have 2 pages.Example fix using useEffect
+ // Reset to page 1 when filters change + useEffect(() => { + dispatchPagination({ type: 'SET_PAGE', payload: 1 }) + }, [searchQuery, activeLevelFilters.size, timeRange, selectedSeverity]) + const filteredLogs = useMemo(() => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/routes/logs.tsx` around lines 285 - 302, When filters change the current page can become invalid; add an effect to reset pagination by calling setCurrentPage(1) whenever searchQuery or activeLevelFilters change. Implement a useEffect that depends on [searchQuery, activeLevelFilters] (or [searchQuery, activeLevelFilters, logs] if you prefer) and inside call setCurrentPage(1); reference the existing filteredLogs, currentPage and setCurrentPage state variables in the file to locate where to add the effect.packages/console-frontend/src/routes/triggers.tsx (2)
467-467: Unused underscore variable in filter callback.The underscore in
Object.entries(queryParams).filter(([_, v]) => v)could be replaced with a more explicit pattern or use.filter(([, v]) => v)to avoid the unused variable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/routes/triggers.tsx` at line 467, The filter callback for building queryEntries uses an unused named variable `_` which is unnecessary; update the Object.entries call that produces queryEntries to destructure only the used second element (e.g., use ([, v]) or similar) so the unused key is not declared, i.e., change Object.entries(queryParams).filter(([_, v]) => v) to use the ignored-first-element pattern and keep the filter logic (reference: queryEntries and queryParams).
155-176: Missing default case intriggersUiReducer.Unlike
httpRequestReducerandinvokeReducer, thetriggersUiReducerlacks a default case. While TypeScript's exhaustiveness checking should catch unhandled actions at compile time, adding a default return ensures runtime safety.🔧 Proposed fix
case 'SET_EVENT_PAYLOAD': return { ...state, eventPayload: action.payload } + default: + return state } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/routes/triggers.tsx` around lines 155 - 176, triggersUiReducer is missing a default branch so unrecognized action types can return undefined at runtime; update the switch in function triggersUiReducer to include a default case that returns the current state (e.g., return state) to ensure runtime safety for unknown TriggersUiAction values, and optionally include an exhaustive type guard (using a never/assertUnreachable pattern) if you want compile-time checks in addition to the default return.packages/console-frontend/src/components/traces/FlameGraph.tsx (3)
93-100: Self-time calculation may be incorrect for overlapping child spans.The current algorithm sums all children's durations and subtracts from parent duration. This assumes children execute sequentially without overlap. For concurrent/parallel spans, this could produce incorrect (potentially negative, though clamped to 0) self-time values.
If children can overlap in time, consider computing self-time based on actual time intervals rather than summing durations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/components/traces/FlameGraph.tsx` around lines 93 - 100, The self-time logic for FlameGraph is wrong for overlapping children: instead of summing child durations in the loop over spanMap.values(), compute the total covered time by merging child time intervals (use each child.span.start_ms and child.span.duration_ms to form [start,end), clamp those intervals to the parent span bounds, sort and merge overlapping intervals to get the union length), then set node.selfTime = Math.max(0, node.span.duration_ms - coveredUnionLength). Update the block that currently accumulates childrenDuration to perform interval clipping/merging for node.children and subtract the merged coverage from node.span.duration_ms.
427-435: Wheel panning also lacks upper bound check.Similar to minimap click, horizontal panning via wheel has no maximum bound, allowing users to pan past the content.
🔧 Proposed fix
} else { - dispatch({ type: 'SET_PAN', panOffset: Math.max(0, panOffset + e.deltaY) }) + const container = containerRef.current + if (!container) return + const width = container.clientWidth + const graphWidth = (width - PADDING * 2) * zoomLevel + const maxOffset = Math.max(0, graphWidth - (width - PADDING * 2)) + dispatch({ type: 'SET_PAN', panOffset: Math.max(0, Math.min(maxOffset, panOffset + e.deltaY)) }) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/components/traces/FlameGraph.tsx` around lines 427 - 435, handleWheel allows panOffset to grow without an upper bound; clamp the new pan value before dispatching SET_PAN. In handleWheel, compute the maximum allowable pan (e.g. maxPan = Math.max(0, totalContentWidth - viewportWidth) or call the existing getMaxPan helper if present), then replace Math.max(0, panOffset + e.deltaY) with a clamped value like Math.min(maxPan, Math.max(0, panOffset + e.deltaY)) and dispatch that clamped value for 'SET_PAN' so horizontal wheel panning cannot move past the content.
437-451: Minimap click panning could exceed valid bounds.The
newOffsetcalculation can produce values that pan beyond the graph's content when clicking near the edges of the minimap. WhileMath.max(0, newOffset)prevents negative values, there's no upper bound check.🔧 Proposed fix to add upper bound
// Center viewport on click position const clickPercent = (clickX - PADDING) / (width - PADDING * 2) const newOffset = clickPercent * graphWidth - (width - PADDING * 2) / 2 - dispatch({ type: 'SET_PAN', panOffset: Math.max(0, newOffset) }) + const maxOffset = graphWidth - (width - PADDING * 2) + dispatch({ type: 'SET_PAN', panOffset: Math.max(0, Math.min(maxOffset, newOffset)) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/components/traces/FlameGraph.tsx` around lines 437 - 451, The minimap click handler (handleMinimapClick) currently only clamps negative pan values but can exceed the maximum scrollable range; compute the viewportWidth as (width - PADDING * 2), compute graphWidth as already done, then compute maxOffset = Math.max(0, graphWidth - viewportWidth) and clamp newOffset into [0, maxOffset] before dispatching SET_PAN via dispatch({ type: 'SET_PAN', panOffset: clampedOffset }); use existing minimapRef, containerRef, PADDING and zoomLevel symbols to locate the values to compute the clamp.packages/console-frontend/src/routes/streams.tsx (1)
130-192: Consider extracting aMAX_MESSAGESconstant.The 1000 cap is a magic number inside the reducer; a named constant makes it easier to tune and reuse.
♻️ Suggested refactor
+const MAX_MESSAGES = 1000 + function messagesReducer(state: MessagesState, action: MessagesAction): MessagesState { switch (action.type) { case 'add_message': { const { entry } = action return { - messages: [entry, ...state.messages].slice(0, 1000), + messages: [entry, ...state.messages].slice(0, MAX_MESSAGES), stats: { ...state.stats, totalMessages: state.stats.totalMessages + 1,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-frontend/src/routes/streams.tsx` around lines 130 - 192, Introduce a named constant MAX_MESSAGES (e.g., const MAX_MESSAGES = 1000) near the top of this module and replace the hard-coded 1000 in messagesReducer (the .slice(0, 1000) call) with MAX_MESSAGES; ensure any future uses (if needed) reference MAX_MESSAGES and keep INITIAL_MESSAGES_STATE unchanged, exporting MAX_MESSAGES if it needs to be reused elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/console-frontend/src/components/filters/TimeRangeFilter.tsx`:
- Around line 390-400: The overlay's onKeyDown won't fire because it isn't
focusable; modify TimeRangeFilter to add a document-level keydown listener via
useEffect that watches isOpen and, when isOpen is true, attaches a handler that
calls setIsOpen(false) on Escape and removes the listener on cleanup (or when
isOpen becomes false); remove the onKeyDown prop from the overlay <div> and keep
the click handler for click-away behavior while ensuring the useEffect
references setIsOpen and isOpen so the Escape key reliably closes the panel.
In `@packages/console-frontend/src/components/layout/Sidebar.tsx`:
- Line 123: The img tag in Sidebar.tsx uses a hardcoded root-relative path
"/iii-white1.1.svg" (appears in the <img src=...> usage and again later) which
will break under a Vite base path; update both image src usages in the Sidebar
component to prefix the file with import.meta.env.BASE_URL (e.g., set src to
`${import.meta.env.BASE_URL}iii-white1.1.svg` or build the path via string
concatenation) so asset URLs respect the configured base path.
In `@packages/console-frontend/src/components/traces/SpanLogsTab.tsx`:
- Around line 55-57: The list item key in SpanLogsTab.tsx currently uses
`${event.name}-${event.timestamp}` which can collide for SpanEvent objects
lacking a unique id; update the key generation in the map rendering where you
use `event` (the map callback inside SpanLogsTab) to include the iteration index
(or another unique identifier if available) so keys become unique (e.g., combine
event.name, event.timestamp, and the map index) to prevent React key collisions
and mis-renders.
In `@packages/console-frontend/src/components/traces/WaterfallChart.tsx`:
- Around line 141-144: The localStorage access in the spanColWidth initialiser
can throw (e.g., private mode), so update the useState initializer in
WaterfallChart.tsx to wrap localStorage.getItem('iii-span-col-width') in a
try-catch, parse the value only if retrieved and valid (fallback to 300 on any
error or NaN), and similarly wrap any future localStorage.setItem calls for
spanColWidth in try-catch to avoid runtime errors; reference the spanColWidth
and setSpanColWidth state variables to locate the code to change.
In `@packages/console-frontend/src/routes/index.tsx`:
- Around line 650-661: The Connection Issue UI block controlled by hasError in
routes/index.tsx is unreachable because the loader always redirects; remove or
relocate this error handling and implement it in the functions route instead.
Specifically, delete or disable the unreachable JSX that renders the yellow
"Connection Issue" card (the hasError conditional) and add equivalent error
handling and UI rendering in the /functions route's component or loader error
path (update the functions route loader or component to set and render an error
state). Ensure any state or props names (hasError) are wired to the functions
route logic so the UI becomes reachable when that route detects the engine
connection failure.
In `@packages/console-frontend/src/routes/streams.tsx`:
- Around line 456-458: The clearMessages handler currently clears messages and
resets selection but doesn't reset pagination, causing an empty view if the user
was on a later page; update the clearMessages function (where messagesDispatch
and setSelectedMessageId are called) to also reset pagination by calling
setCurrentPage(1) so the Pagination component (which slices filteredMessages by
currentPage and pageSize) shows page 1 immediately after clearing.
---
Outside diff comments:
In `@packages/console-frontend/src/routes/index.tsx`:
- Around line 29-43: The loader for createFileRoute('/') always throws
redirect({ to: '/functions' }) so DashboardPage is never rendered; either remove
the DashboardPage component and its related code, or make the redirect
conditional in the loader (e.g., check a flag/user state or query param) and
only redirect when appropriate; if you keep the loader prefetch logic, await
Promise.allSettled([...queryClient.prefetchQuery(statusQuery),
queryClient.prefetchQuery(functionsQuery()),
queryClient.prefetchQuery(triggersQuery()),
queryClient.prefetchQuery(streamsQuery()),
queryClient.prefetchQuery(metricsHistoryQuery(100))]) before redirecting so
prefetch completes, and ensure you return appropriate loader data instead of
unconditionally throwing from loader when DashboardPage must be reachable.
In `@packages/console-frontend/src/routes/states.tsx`:
- Around line 231-251: handleEditItem is incorrectly toggling the add-modal
saving state via dispatchAddModal({ type: 'START_SAVE' }) / 'SAVE_DONE'; change
it to track saving on the edit reducer instead: add a saving boolean to
EditState (or a separate editLoading flag) and dispatch edit actions (e.g.,
dispatchEdit({ type: 'START_SAVE' }) and dispatchEdit({ type: 'SAVE_DONE' }))
inside handleEditItem around the async setStateItem call, and remove the
dispatchAddModal calls so the add modal saving state is not affected.
---
Nitpick comments:
In `@packages/console-frontend/src/api/config-provider.tsx`:
- Around line 38-44: The useQuery in ConfigProvider is currently missing a
staleTime and uses retry: true (infinite retries); update the query options in
ConfigProvider (the useQuery call) to set staleTime: Infinity (or a large ms
value) to avoid refetching session-static console config, and change retry from
true to a finite count such as retry: 3 (or an appropriate number) to prevent
infinite retry loops.
- Around line 33-35: The query function currently calls setConfig(data) after
parsing the response, causing side effects on every fetch/retry; move the state
mutation out of the query function and into an onSuccess callback (or the
component/caller that invokes useQuery) so the queryFn only returns the fetched
ConsoleConfig and setConfig is invoked once or guarded for idempotency;
specifically, remove the setConfig(data) call from the fetch logic in
config-provider.tsx and instead call setConfig from the useQuery onSuccess
handler (or wrap setConfig to no-op when the value is unchanged) so setConfig
and any re-render/persistence side effects are controlled.
In `@packages/console-frontend/src/components/filters/TimeRangeFilter.tsx`:
- Around line 318-330: The label/input pair in the TimeRangeFilter component
uses a hardcoded id ("time-range-start") which can collide when multiple
instances mount; import and call React.useId() (or useId from React) inside the
TimeRangeFilter component to create a unique base id (e.g., idBase = useId())
and replace the static id and htmlFor with a scoped id like `${idBase}-start`
(and similarly update any matching end input if present) so the Input's id and
the label's htmlFor remain unique per component instance while keeping the
existing value/onChange (customStart, setCustomStart) intact.
In `@packages/console-frontend/src/components/traces/FlameGraph.tsx`:
- Around line 93-100: The self-time logic for FlameGraph is wrong for
overlapping children: instead of summing child durations in the loop over
spanMap.values(), compute the total covered time by merging child time intervals
(use each child.span.start_ms and child.span.duration_ms to form [start,end),
clamp those intervals to the parent span bounds, sort and merge overlapping
intervals to get the union length), then set node.selfTime = Math.max(0,
node.span.duration_ms - coveredUnionLength). Update the block that currently
accumulates childrenDuration to perform interval clipping/merging for
node.children and subtract the merged coverage from node.span.duration_ms.
- Around line 427-435: handleWheel allows panOffset to grow without an upper
bound; clamp the new pan value before dispatching SET_PAN. In handleWheel,
compute the maximum allowable pan (e.g. maxPan = Math.max(0, totalContentWidth -
viewportWidth) or call the existing getMaxPan helper if present), then replace
Math.max(0, panOffset + e.deltaY) with a clamped value like Math.min(maxPan,
Math.max(0, panOffset + e.deltaY)) and dispatch that clamped value for 'SET_PAN'
so horizontal wheel panning cannot move past the content.
- Around line 437-451: The minimap click handler (handleMinimapClick) currently
only clamps negative pan values but can exceed the maximum scrollable range;
compute the viewportWidth as (width - PADDING * 2), compute graphWidth as
already done, then compute maxOffset = Math.max(0, graphWidth - viewportWidth)
and clamp newOffset into [0, maxOffset] before dispatching SET_PAN via
dispatch({ type: 'SET_PAN', panOffset: clampedOffset }); use existing
minimapRef, containerRef, PADDING and zoomLevel symbols to locate the values to
compute the clamp.
In `@packages/console-frontend/src/components/traces/TraceMap.tsx`:
- Around line 100-117: The angle calculation for nodes uses the pre-filter index
i and nodeCount which can produce gaps if any items are filtered out; update
TraceMap.tsx so angles are computed after filtering: first build the filtered
list of valid services (use serviceMap.get(name) to filter and produce a
ServiceNode-like partial), then map that filtered array with its own index to
compute angle using its length as the nodeCount, or alternatively assert
non-null with serviceMap.get(name)! and remove the defensive filter (add a
comment explaining the invariant). Ensure you update references to nodes,
serviceNames, nodeCount, and the angle computation to use the post-filter
array/length so layout evenly redistributes.
In `@packages/console-frontend/src/components/traces/WaterfallChart.tsx`:
- Around line 270-284: In WaterfallChart.tsx update the resize handle element:
change role="slider" to role="separator", set aria-orientation="vertical" (since
it separates columns), and remove the slider-specific attributes aria-valuemin,
aria-valuemax and aria-valuenow; keep tabIndex, the event handlers
(startColResize, onDoubleClick that calls setSpanColWidth(300)), title and
styling so keyboard/mouse accessibility is preserved.
In `@packages/console-frontend/src/components/ui/card.tsx`:
- Around line 27-37: The CardTitle forwardRef has a type mismatch: the ref is
declared as HTMLParagraphElement while the component renders an <h3> (a
heading). Update the generics and ref type on CardTitle to use
HTMLHeadingElement and React.HTMLAttributes<HTMLHeadingElement> so the ref and
props match the rendered <h3> element (update the forwardRef signature and ref
generic accordingly).
In `@packages/console-frontend/src/routes/functions.tsx`:
- Around line 226-229: The dispatchInvocation call that sets the invocation
result is missing the HTTP status field defined on InvocationResult; update the
object passed to dispatchInvocation (inside the function that calls
invokeFunctionApi and currently uses dispatchInvocation({ type: 'SET_RESULT',
result: { success: result.success, duration, data: result.data, error:
result.error } })) to include status (e.g., status: result.status or the
response HTTP status returned by invokeFunctionApi) so the SET_RESULT payload
populates InvocationResult.status for better debugging.
In `@packages/console-frontend/src/routes/logs.tsx`:
- Around line 285-302: When filters change the current page can become invalid;
add an effect to reset pagination by calling setCurrentPage(1) whenever
searchQuery or activeLevelFilters change. Implement a useEffect that depends on
[searchQuery, activeLevelFilters] (or [searchQuery, activeLevelFilters, logs] if
you prefer) and inside call setCurrentPage(1); reference the existing
filteredLogs, currentPage and setCurrentPage state variables in the file to
locate where to add the effect.
In `@packages/console-frontend/src/routes/streams.tsx`:
- Around line 130-192: Introduce a named constant MAX_MESSAGES (e.g., const
MAX_MESSAGES = 1000) near the top of this module and replace the hard-coded 1000
in messagesReducer (the .slice(0, 1000) call) with MAX_MESSAGES; ensure any
future uses (if needed) reference MAX_MESSAGES and keep INITIAL_MESSAGES_STATE
unchanged, exporting MAX_MESSAGES if it needs to be reused elsewhere.
In `@packages/console-frontend/src/routes/traces.tsx`:
- Around line 284-317: The mousemove handler currently calls setPanelWidths on
every event causing excessive re-renders; throttle updates using
requestAnimationFrame by adding an RAF loop inside the useEffect: in onMouseMove
(used with isResizingRef and resizeStartRef) capture the desired width
calculation (using clampPanelWidth, PANEL_MIN_WIDTH, totalWidth, etc.) into a
local "next" value and schedule a single requestAnimationFrame to call
setPanelWidths(trace/ span) if one isn’t already pending; store the raf id in a
ref so you can cancel it on cleanup and when onMouseUp clears isResizingRef and
cancels any pending RAF to avoid orphaned frames.
In `@packages/console-frontend/src/routes/triggers.tsx`:
- Line 467: The filter callback for building queryEntries uses an unused named
variable `_` which is unnecessary; update the Object.entries call that produces
queryEntries to destructure only the used second element (e.g., use ([, v]) or
similar) so the unused key is not declared, i.e., change
Object.entries(queryParams).filter(([_, v]) => v) to use the
ignored-first-element pattern and keep the filter logic (reference: queryEntries
and queryParams).
- Around line 155-176: triggersUiReducer is missing a default branch so
unrecognized action types can return undefined at runtime; update the switch in
function triggersUiReducer to include a default case that returns the current
state (e.g., return state) to ensure runtime safety for unknown TriggersUiAction
values, and optionally include an exhaustive type guard (using a
never/assertUnreachable pattern) if you want compile-time checks in addition to
the default return.
| {isOpen && ( | ||
| // biome-ignore lint/a11y/noStaticElementInteractions: click-away overlay with keyboard support | ||
| <div | ||
| role="presentation" | ||
| className="fixed inset-0 z-40" | ||
| onClick={() => setIsOpen(false)} | ||
| onKeyDown={(e) => { | ||
| if (e.key === 'Escape') setIsOpen(false) | ||
| }} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
Escape key handler won't work — the overlay is not focusable.
The onKeyDown handler on this <div> will never fire because the element has no tabIndex and cannot receive focus. Keyboard events only fire on the focused element (or bubble from descendants). The click-away works, but Escape key dismissal does not.
🔧 Suggested fix using a document-level listener
Replace the onKeyDown on the overlay with a useEffect:
+import { useCallback, useEffect, useMemo, useState } from 'react'+ // Close dropdown on Escape key
+ useEffect(() => {
+ if (!isOpen) return
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') setIsOpen(false)
+ }
+ document.addEventListener('keydown', handleKeyDown)
+ return () => document.removeEventListener('keydown', handleKeyDown)
+ }, [isOpen])Then simplify the overlay:
{isOpen && (
- // biome-ignore lint/a11y/noStaticElementInteractions: click-away overlay with keyboard support
<div
role="presentation"
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
- onKeyDown={(e) => {
- if (e.key === 'Escape') setIsOpen(false)
- }}
/>
)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/console-frontend/src/components/filters/TimeRangeFilter.tsx` around
lines 390 - 400, The overlay's onKeyDown won't fire because it isn't focusable;
modify TimeRangeFilter to add a document-level keydown listener via useEffect
that watches isOpen and, when isOpen is true, attaches a handler that calls
setIsOpen(false) on Escape and removes the listener on cleanup (or when isOpen
becomes false); remove the onKeyDown prop from the overlay <div> and keep the
click handler for click-away behavior while ensuring the useEffect references
setIsOpen and isOpen so the Escape key reliably closes the panel.
| <div className="px-5 py-4 md:py-4.5 border-b border-border flex items-center justify-between"> | ||
| <div className="flex items-center gap-3"> | ||
| <IIILogo className="w-6 h-6 text-white" /> | ||
| <img src="/iii-white1.1.svg" alt="iii logo" className="w-6 h-6" /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check for base path / asset prefix configuration and existing asset URL conventions
echo "=== Finding bundler config files ==="
fd -a 'vite.config|webpack.config|next.config|package.json' | head -20
echo -e "\n=== Checking bundler configuration for base path ==="
rg -n "base:|publicPath|assetPrefix|homepage" \
-g 'vite.config.*' -g 'webpack.config.*' -g 'next.config.*' -g 'package.json'
echo -e "\n=== Checking for BASE_URL or PUBLIC_URL usage ==="
rg -n "BASE_URL|PUBLIC_URL|assetPrefix" -g '*.{ts,tsx,js,jsx}' | head -20
echo -e "\n=== Finding all SVG asset paths ==="
rg -n "/iii-white1\.1\.svg|/iii-black1\.1\.svg" -g '*.{ts,tsx,js,jsx,html,css,scss}'Repository: iii-hq/console
Length of output: 668
🏁 Script executed:
#!/bin/bash
echo "=== Vite configuration ==="
cat -n packages/console-frontend/vite.config.ts
echo -e "\n=== Check public directory structure ==="
find packages/console-frontend/public -type f 2>/dev/null | head -20
echo -e "\n=== Look for other img src usages ==="
rg -n '<img\s+src=' packages/console-frontend/src -g '*.{ts,tsx}' -A 1 | head -40
echo -e "\n=== Look for asset imports ==="
rg -n "import.*\.(svg|png|jpg|gif|webp)" packages/console-frontend/src -g '*.{ts,tsx}' | head -20
echo -e "\n=== Check how other SVG/images are referenced ==="
rg -n "import.*svg|from.*\.svg" packages/console-frontend/src -g '*.{ts,tsx}' | head -20Repository: iii-hq/console
Length of output: 2912
🏁 Script executed:
#!/bin/bash
echo "=== Check Vite/package versions ==="
cat packages/console-frontend/package.json | jq '.devDependencies | {vite, react, "@vitejs/plugin-react"}'
echo -e "\n=== Verify BASE_URL is Vite-provided ==="
rg -n "import.meta.env" packages/console-frontend/src -g '*.{ts,tsx}' | head -10Repository: iii-hq/console
Length of output: 441
Use import.meta.env.BASE_URL for asset paths to ensure compatibility with sub-path deployments.
Lines 123 and 266 hardcode /iii-white1.1.svg. While this works with the current configuration, if the app is deployed under a base path (e.g., by setting base in Vite config), these root-relative URLs would 404. Use import.meta.env.BASE_URL to prefix asset paths so they adapt to any deployment configuration.
🔧 Proposed adjustment
- <img src="/iii-white1.1.svg" alt="iii logo" className="w-6 h-6" />
+ <img
+ src={`${import.meta.env.BASE_URL}iii-white1.1.svg`}
+ alt="iii logo"
+ className="w-6 h-6"
+ />- <img src="/iii-white1.1.svg" alt="iii logo" className="w-5 h-5" />
+ <img
+ src={`${import.meta.env.BASE_URL}iii-white1.1.svg`}
+ alt="iii logo"
+ className="w-5 h-5"
+ />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/console-frontend/src/components/layout/Sidebar.tsx` at line 123, The
img tag in Sidebar.tsx uses a hardcoded root-relative path "/iii-white1.1.svg"
(appears in the <img src=...> usage and again later) which will break under a
Vite base path; update both image src usages in the Sidebar component to prefix
the file with import.meta.env.BASE_URL (e.g., set src to
`${import.meta.env.BASE_URL}iii-white1.1.svg` or build the path via string
concatenation) so asset URLs respect the configured base path.
| <div | ||
| key={`${event.timestamp}-${index}`} | ||
| key={`${event.name}-${event.timestamp}`} | ||
| className={`rounded-lg border overflow-hidden ${ |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat packages/console-frontend/src/components/traces/SpanLogsTab.tsx | head -100Repository: iii-hq/console
Length of output: 3901
🏁 Script executed:
rg -n "interface VisualizationSpan|type VisualizationSpan" packages --type ts --type tsxRepository: iii-hq/console
Length of output: 85
🏁 Script executed:
rg -n "interface VisualizationSpan|type VisualizationSpan" packagesRepository: iii-hq/console
Length of output: 226
🏁 Script executed:
cat packages/console-frontend/src/lib/traceTransform.tsRepository: iii-hq/console
Length of output: 8300
🏁 Script executed:
fd -t f -name "*.ts" packages | xargs rg -n "interface StoredSpan|type StoredSpan" -A 20 | head -50Repository: iii-hq/console
Length of output: 2038
🏁 Script executed:
rg -n "interface SpanEvent|type SpanEvent" packagesRepository: iii-hq/console
Length of output: 146
🏁 Script executed:
sed -n '8,20p' packages/console-frontend/src/api/observability/traces.tsRepository: iii-hq/console
Length of output: 298
Potential React key collisions (name + timestamp may not be unique).
SpanEvent has no unique identifier field, so if multiple events share the same name and timestamp, React will see duplicate keys and may mis-render or drop updates. Add the index to guarantee uniqueness:
🔧 Recommended fix
- key={`${event.name}-${event.timestamp}`}
+ key={`${event.name}-${event.timestamp}-${index}`}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/console-frontend/src/components/traces/SpanLogsTab.tsx` around lines
55 - 57, The list item key in SpanLogsTab.tsx currently uses
`${event.name}-${event.timestamp}` which can collide for SpanEvent objects
lacking a unique id; update the key generation in the map rendering where you
use `event` (the map callback inside SpanLogsTab) to include the iteration index
(or another unique identifier if available) so keys become unique (e.g., combine
event.name, event.timestamp, and the map index) to prevent React key collisions
and mis-renders.
| const [spanColWidth, setSpanColWidth] = useState(() => { | ||
| const saved = localStorage.getItem('iii-span-col-width') | ||
| return saved ? Number.parseInt(saved, 10) : 300 | ||
| }) |
There was a problem hiding this comment.
Add error handling for localStorage access.
localStorage.getItem can throw in private browsing mode or when storage is full. Wrap in try-catch to prevent runtime errors.
Suggested fix
const [spanColWidth, setSpanColWidth] = useState(() => {
- const saved = localStorage.getItem('iii-span-col-width')
- return saved ? Number.parseInt(saved, 10) : 300
+ try {
+ const saved = localStorage.getItem('iii-span-col-width')
+ return saved ? Number.parseInt(saved, 10) : 300
+ } catch {
+ return 300
+ }
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [spanColWidth, setSpanColWidth] = useState(() => { | |
| const saved = localStorage.getItem('iii-span-col-width') | |
| return saved ? Number.parseInt(saved, 10) : 300 | |
| }) | |
| const [spanColWidth, setSpanColWidth] = useState(() => { | |
| try { | |
| const saved = localStorage.getItem('iii-span-col-width') | |
| return saved ? Number.parseInt(saved, 10) : 300 | |
| } catch { | |
| return 300 | |
| } | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/console-frontend/src/components/traces/WaterfallChart.tsx` around
lines 141 - 144, The localStorage access in the spanColWidth initialiser can
throw (e.g., private mode), so update the useState initializer in
WaterfallChart.tsx to wrap localStorage.getItem('iii-span-col-width') in a
try-catch, parse the value only if retrieved and valid (fallback to 300 on any
error or NaN), and similarly wrap any future localStorage.setItem calls for
spanColWidth in try-catch to avoid runtime errors; reference the spanColWidth
and setSpanColWidth state variables to locate the code to change.
| {hasError && ( | ||
| <div className="bg-yellow/10 border border-yellow/30 rounded-lg p-3 flex items-center gap-3"> | ||
| <AlertTriangle className="w-5 h-5 text-yellow flex-shrink-0" /> | ||
| <div> | ||
| <p className="text-sm font-medium text-yellow">Connection Issue</p> | ||
| <p className="text-xs text-muted"> | ||
| Unable to connect to the iii engine. Check that it's running on the expected host and | ||
| port. | ||
| </p> | ||
| </div> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
Connection Issue card relies on unreachable code path.
Since the loader always redirects, this error handling UI will never be displayed to users. If error handling is needed for the /functions route instead, it should be implemented there.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/console-frontend/src/routes/index.tsx` around lines 650 - 661, The
Connection Issue UI block controlled by hasError in routes/index.tsx is
unreachable because the loader always redirects; remove or relocate this error
handling and implement it in the functions route instead. Specifically, delete
or disable the unreachable JSX that renders the yellow "Connection Issue" card
(the hasError conditional) and add equivalent error handling and UI rendering in
the /functions route's component or loader error path (update the functions
route loader or component to set and render an error state). Ensure any state or
props names (hasError) are wired to the functions route logic so the UI becomes
reachable when that route detects the engine connection failure.
| const clearMessages = () => { | ||
| setMessages([]) | ||
| setStats({ | ||
| totalMessages: 0, | ||
| inbound: 0, | ||
| outbound: 0, | ||
| totalBytes: 0, | ||
| latency: stats.latency, | ||
| lastPingTime: stats.lastPingTime, | ||
| }) | ||
| messagesDispatch({ type: 'clear', latency: stats.latency, lastPingTime: stats.lastPingTime }) | ||
| setSelectedMessageId(null) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether Pagination clamps currentPage to totalPages.
set -euo pipefail
files=$(fd -a 'pagination\.(t|j)sx?$')
if [ -z "$files" ]; then
echo "No pagination component found."
exit 1
fi
while IFS= read -r f; do
echo "----- $f -----"
sed -n '1,200p' "$f"
done <<< "$files"Repository: iii-hq/console
Length of output: 3642
🏁 Script executed:
#!/bin/bash
# Check the streams.tsx file around clearMessages and currentPage usage
set -euo pipefail
# First, find and examine the relevant section of streams.tsx
FILE="packages/console-frontend/src/routes/streams.tsx"
if [ ! -f "$FILE" ]; then
echo "File not found: $FILE"
exit 1
fi
# Get total lines
TOTAL_LINES=$(wc -l < "$FILE")
echo "Total lines in $FILE: $TOTAL_LINES"
echo ""
# Show the clearMessages function and surrounding context
echo "----- Around clearMessages (lines 450-470) -----"
sed -n '450,470p' "$FILE"
echo ""
# Look for currentPage state declaration
echo "----- Searching for currentPage state -----"
rg -n 'currentPage|setCurrentPage' "$FILE" | head -20
echo ""
# Look for how messages are filtered/paginated
echo "----- Searching for pagination logic with currentPage -----"
rg -n -B2 -A2 'currentPage.*pageSize\|pageSize.*currentPage' "$FILE" | head -40Repository: iii-hq/console
Length of output: 1178
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="packages/console-frontend/src/routes/streams.tsx"
# Show the pagination calculation logic
echo "----- Pagination calculation (lines 440-450) -----"
sed -n '440,450p' "$FILE"
echo ""
# Show more context around currentPage state and usage
echo "----- currentPage state and related logic (lines 220-230) -----"
sed -n '220,230p' "$FILE"
echo ""
# Look for the useMemo that uses filteredMessages and currentPage
echo "----- useMemo with currentPage (lines 438-455) -----"
sed -n '438,455p' "$FILE"
echo ""
# Find how paginatedMessages is used in rendering
echo "----- Search for paginatedMessages usage -----"
rg -n 'paginatedMessages' "$FILE"Repository: iii-hq/console
Length of output: 2054
Reset pagination when clearing messages.
When the user clears messages from a later page (e.g., page 5), currentPage remains unchanged. Since the Pagination component does not clamp internally and uses filteredMessages.slice((currentPage - 1) * pageSize, ...), the list will be empty until enough new messages arrive to fill that page. Resetting to page 1 ensures the list displays correctly immediately after clearing.
Suggested fix
const clearMessages = () => {
messagesDispatch({ type: 'clear', latency: stats.latency, lastPingTime: stats.lastPingTime })
setSelectedMessageId(null)
+ setCurrentPage(1)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const clearMessages = () => { | |
| setMessages([]) | |
| setStats({ | |
| totalMessages: 0, | |
| inbound: 0, | |
| outbound: 0, | |
| totalBytes: 0, | |
| latency: stats.latency, | |
| lastPingTime: stats.lastPingTime, | |
| }) | |
| messagesDispatch({ type: 'clear', latency: stats.latency, lastPingTime: stats.lastPingTime }) | |
| setSelectedMessageId(null) | |
| const clearMessages = () => { | |
| messagesDispatch({ type: 'clear', latency: stats.latency, lastPingTime: stats.lastPingTime }) | |
| setSelectedMessageId(null) | |
| setCurrentPage(1) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/console-frontend/src/routes/streams.tsx` around lines 456 - 458, The
clearMessages handler currently clears messages and resets selection but doesn't
reset pagination, causing an empty view if the user was on a later page; update
the clearMessages function (where messagesDispatch and setSelectedMessageId are
called) to also reset pagination by calling setCurrentPage(1) so the Pagination
component (which slices filteredMessages by currentPage and pageSize) shows page
1 immediately after clearing.
Pull Request
Summary
This PR introduces trace UI improvements, new branding assets, and refactors configuration fetching to use React Query across the iii-console frontend.
Key changes
iii-black1.1.svgandiii-white1.1.svgtopublic/for brandinguseState/useEffectconfig fetching with TanStack React Query (useQuery), including retry logic and simplified loading/error statesType of change
Checklist
Additional context
queryKey: ['console-config']withretry: trueandretryDelay: 3000Summary by CodeRabbit
Release Notes
New Features
Improvements
Bug Fixes