feat: refactor routes and components for functions and triggers#16
feat: refactor routes and components for functions and triggers#16andersonleal merged 1 commit intomainfrom
Conversation
- Introduced new routes for Functions and Triggers, replacing the previous Handlers route. - Updated Sidebar and routeTree to reflect the new structure, including the addition of Triggers. - Enhanced the Functions page with improved UI for function invocation and management. - Removed deprecated Handlers route and adjusted related components for consistency. - Updated various components to improve layout and styling across the application. feat: add /_console/invoke endpoint for direct function invocation Registers engine::console::invoke bridge function and POST /_console/invoke HTTP trigger, enabling the frontend to invoke any function by ID via the bridge SDK (same mechanism iii-tui uses). feat: add invoke play button to function list cards feat: add direct invoke UI for CORE and non-HTTP functions fix: strip leading slash from api_path to prevent double-slash in URLs When HTTP triggers return api_path with a leading slash (e.g. /todo), the UI was producing double-slashes like http://host:port//todo.
📝 WalkthroughWalkthroughThe PR introduces health-based engine status UI with a detailed monitoring panel in the Sidebar, restructures routing by renaming Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as Functions/Triggers Page
participant API as Console API
participant Handler as Rust Handler
participant Engine as Engine
User->>UI: Select function/trigger & enter params
UI->>UI: Parse JSON request body
alt Valid JSON
UI->>API: POST /invoke with function_id & params
API->>Handler: handle_invoke(function_id, input)
Handler->>Handler: Extract function_id & default data
Handler->>Engine: Call function with 30s timeout
Engine-->>Handler: Return result
Handler-->>API: Success/Error response
API-->>UI: Invocation result
UI->>UI: Display result & duration
User-->>UI: View result in JsonViewer
else Invalid JSON
UI-->>User: Show JSON validation error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 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: 2
🧹 Nitpick comments (8)
packages/console-frontend/src/routes/triggers.tsx (1)
158-162: Consider handling clipboard API errors.
navigator.clipboard.writeTextcan reject on certain browsers or when the page doesn't have focus. A try-catch or.catch()would prevent unhandled promise rejections.♻️ Proposed fix
const copyToClipboard = (text: string, key: string) => { - navigator.clipboard.writeText(text) - setCopied(key) - setTimeout(() => setCopied(null), 2000) + navigator.clipboard.writeText(text).then(() => { + setCopied(key) + setTimeout(() => setCopied(null), 2000) + }).catch(() => { + // Clipboard API failed, ignore silently + }) }🤖 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 158 - 162, The copyToClipboard function calls navigator.clipboard.writeText without handling rejections; update copyToClipboard to await or attach a .catch() to navigator.clipboard.writeText so any errors are handled, only call setCopied(key) on success, and on failure log the error (or show a user-facing notification) and avoid calling setCopied; reference the copyToClipboard function and navigator.clipboard.writeText when making the change.packages/console-frontend/src/routes/functions.tsx (1)
110-114: Consider handling clipboard API errors.Same pattern as
triggers.tsx-navigator.clipboard.writeTextcan reject. Consider extracting this into a shared utility with error handling.🤖 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 110 - 114, The copyToClipboard function currently calls navigator.clipboard.writeText without handling rejections; update it to await or attach .then/.catch to navigator.clipboard.writeText(text), only call setCopied(key) and schedule clearing on success, and log or surface errors on failure. Extract this logic into a shared utility (e.g., export a safeCopyToClipboard or writeTextToClipboard helper) and replace the inline copyToClipboard in functions.tsx (and mirror the change in triggers.tsx) so both components reuse the promise-handling, error-logging, and success-only setCopied behavior.packages/console-rust/src/bridge/functions.rs (1)
579-609: Consider applying format validation to thefunction_idparameter.The
handle_invokefunction accepts a user-provided function identifier without validation, unlike other handlers which use hardcoded function names. While the function properly handles invocation errors, applying format validation (similar to the existingvalidate_flow_idpattern) would improve robustness and prevent unexpected invocations.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/console-rust/src/bridge/functions.rs` around lines 579 - 609, The handle_invoke function accepts an unvalidated function_id string; add format validation similar to validate_flow_id to reject malformed IDs before calling bridge.call_with_timeout. Modify handle_invoke to call an existing validate_flow_id-like helper (or add validate_function_id) on the extracted function_id and return error_response(IIIError::Handler(...)) when validation fails, and only proceed to call bridge.call_with_timeout(&function_id, ...) when the ID passes validation; reference the function name handle_invoke, the variable function_id, and the call bridge.call_with_timeout in your changes.packages/console-frontend/src/routes/index.tsx (5)
485-510: Same issue: repeatedstreams.filter((s) => !s.internal)calls.This filter is called 5 times in this section alone. Apply the same memoization pattern:
♻️ Suggested refactor
const userStreams = useMemo( () => streams.filter((s) => !s.internal), [streams], )Then use
userStreamsinstead of inline filters.🤖 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 485 - 510, The code repeatedly calls streams.filter((s) => !s.internal) causing redundant computation; create a memoized variable (e.g., userStreams) using useMemo(() => streams.filter(s => !s.internal), [streams]) and replace all inline filter calls in this JSX block (the map, length checks, slice, and "+N more" calculation) with userStreams to avoid repeated filtering; ensure you import/use React's useMemo in the component and update references like s.id, slice(0,2), and length-based conditional rendering to reference userStreams instead of streams.filter(...).
351-419: Memoize repeated filter operations to avoid redundant iterations.The same filters are called multiple times within the render:
userTriggers.filter((t) => t.trigger_type === 'http')— 5 timesuserTriggers.filter((t) => t.trigger_type === 'cron')— 2 timesuserTriggers.filter((t) => t.trigger_type === 'event')— 2 timesWhile not critical for small datasets, this is inefficient and harder to maintain.
♻️ Suggested refactor: memoize filtered arrays
Add these memoized values near the other
useMemocalls (around line 233):const httpTriggers = useMemo( () => userTriggers.filter((t) => t.trigger_type === 'http'), [userTriggers], ) const cronTriggers = useMemo( () => userTriggers.filter((t) => t.trigger_type === 'cron'), [userTriggers], ) const eventTriggers = useMemo( () => userTriggers.filter((t) => t.trigger_type === 'event'), [userTriggers], )Then replace inline filters with these variables throughout the component.
🤖 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 351 - 419, The render repeatedly calls userTriggers.filter(...) for 'http', 'cron', and 'event', causing redundant iterations; create memoized arrays using useMemo (e.g., const httpTriggers = useMemo(() => userTriggers.filter(t => t.trigger_type === 'http'), [userTriggers]) and similarly cronTriggers and eventTriggers) near the other useMemo calls, then replace all inline userTriggers.filter(...) usages in the component with httpTriggers, cronTriggers, and eventTriggers (also update counts, .slice(0,4), .map and the "+N more" calculation to use these variables).
146-181: Applycursor-pointeronly whenhrefis provided.The content div always has
cursor-pointer(line 148), but when nohrefis passed, the card isn't clickable. This creates a misleading affordance for users.♻️ Suggested fix
function MetricsChart({ title, value, data, color, icon: Icon, trend, href }: MetricsChartProps) { const content = ( - <div className="bg-dark-gray/40 rounded-xl border border-border p-4 transition-all duration-200 group-hover:border-muted/40 group-hover:-translate-y-0.5 cursor-pointer"> + <div className={`bg-dark-gray/40 rounded-xl border border-border p-4 transition-all duration-200 ${href ? 'group-hover:border-muted/40 group-hover:-translate-y-0.5 cursor-pointer' : ''}`}>🤖 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 146 - 181, The MetricsChart component currently applies "cursor-pointer" on the content div regardless of clickability; update MetricsChart so the "cursor-pointer" class is only present when href is provided (either remove it from the static content div and add it to the Link wrapper when rendering the link, or conditionally include it in the content div's className based on href). Locate the MetricsChart function and adjust the className on the div that currently contains "cursor-pointer" (and/or the Link element) so non-link cards do not show a pointer cursor while linked cards retain it.
53-97: Consider using unique gradient IDs to prevent SVG ID collisions.The hardcoded
id="skeleton-pulse"gradient will be duplicated in the DOM if multipleMiniChartcomponents render with insufficient data simultaneously. While the visual impact is minimal since all skeletons share the same animation, SVG ID collisions can cause unexpected behavior in some browsers.♻️ Suggested fix using React's useId
+import { useId } from 'react' + function MiniChart({ data, color, height = 40 }: MiniChartProps) { + const gradientId = useId() + if (data.length < 2) { return ( <svg viewBox={`0 0 100 ${height}`} className="w-full h-8" preserveAspectRatio="none"> <defs> - <linearGradient id="skeleton-pulse" x1="0" y1="0" x2="1" y2="0"> + <linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="0"> ... </linearGradient> </defs> ... <polygon points="0,32 0,28 12,22 25,26 37,18 50,20 62,14 75,18 87,12 100,16 100,32" - fill="url(`#skeleton-pulse`)" + fill={`url(#${gradientId})`} /> </svg> ) }🤖 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 53 - 97, The SVG uses a hardcoded id="skeleton-pulse" which can collide when multiple MiniChart instances render; change the component (e.g., MiniChart in routes/index.tsx) to generate a unique gradient id (use React's useId or a uniqueId helper) and substitute that id for the linearGradient id and the polygon fill url reference (and any other url(#...) references) so each instance gets its own gradient id.
245-248:streamsChartDataproduces a flat line since it uses current count for all history points.Unlike
functionsChartDataandtriggersChartDatawhich read frommetricsHistory(m.functions_count,m.triggers_count), this maps every history entry to the current streams count. The resulting chart will always be a horizontal line.If historical stream counts are available in the metrics data, consider using them. Otherwise, this is fine as-is, but the mini-chart won't show any meaningful trend.
🤖 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 245 - 248, streamsChartData is using the current streams.length for every metricsHistory point, producing a flat line; change the mapping to read the historical stream count from metricsHistory (e.g., use m.streams_count or the appropriate property on each metrics entry) similar to how functionsChartData and triggersChartData use m.functions_count and m.triggers_count, and fall back to streams.filter(s => !s.internal).length only if the historical value is undefined.
🤖 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/routes/functions.tsx`:
- Around line 26-34: The loader passed to createFileRoute for Route (component:
FunctionsPage) starts prefetching with Promise.allSettled([...]) but doesn't
return that promise, so the router won't await it; update the loader to return
the Promise.allSettled(...) result (i.e., return Promise.allSettled([
queryClient.prefetchQuery(functionsQuery()),
queryClient.prefetchQuery(workersQuery) ])) so TanStack Router will await the
prefetches.
In `@packages/console-frontend/src/routes/triggers.tsx`:
- Around line 30-38: The loader for Route invokes Promise.allSettled([...]) but
doesn't return it, so TanStack Router won't wait for
queryClient.prefetchQuery(triggersQuery()) and
queryClient.prefetchQuery(functionsQuery()) to complete; fix by returning the
Promise from the loader (e.g., add a return before Promise.allSettled or make
the loader async and return/await the Promise.allSettled result) so Route.loader
returns the prefetch promise and the router will await the queries.
---
Nitpick comments:
In `@packages/console-frontend/src/routes/functions.tsx`:
- Around line 110-114: The copyToClipboard function currently calls
navigator.clipboard.writeText without handling rejections; update it to await or
attach .then/.catch to navigator.clipboard.writeText(text), only call
setCopied(key) and schedule clearing on success, and log or surface errors on
failure. Extract this logic into a shared utility (e.g., export a
safeCopyToClipboard or writeTextToClipboard helper) and replace the inline
copyToClipboard in functions.tsx (and mirror the change in triggers.tsx) so both
components reuse the promise-handling, error-logging, and success-only setCopied
behavior.
In `@packages/console-frontend/src/routes/index.tsx`:
- Around line 485-510: The code repeatedly calls streams.filter((s) =>
!s.internal) causing redundant computation; create a memoized variable (e.g.,
userStreams) using useMemo(() => streams.filter(s => !s.internal), [streams])
and replace all inline filter calls in this JSX block (the map, length checks,
slice, and "+N more" calculation) with userStreams to avoid repeated filtering;
ensure you import/use React's useMemo in the component and update references
like s.id, slice(0,2), and length-based conditional rendering to reference
userStreams instead of streams.filter(...).
- Around line 351-419: The render repeatedly calls userTriggers.filter(...) for
'http', 'cron', and 'event', causing redundant iterations; create memoized
arrays using useMemo (e.g., const httpTriggers = useMemo(() =>
userTriggers.filter(t => t.trigger_type === 'http'), [userTriggers]) and
similarly cronTriggers and eventTriggers) near the other useMemo calls, then
replace all inline userTriggers.filter(...) usages in the component with
httpTriggers, cronTriggers, and eventTriggers (also update counts, .slice(0,4),
.map and the "+N more" calculation to use these variables).
- Around line 146-181: The MetricsChart component currently applies
"cursor-pointer" on the content div regardless of clickability; update
MetricsChart so the "cursor-pointer" class is only present when href is provided
(either remove it from the static content div and add it to the Link wrapper
when rendering the link, or conditionally include it in the content div's
className based on href). Locate the MetricsChart function and adjust the
className on the div that currently contains "cursor-pointer" (and/or the Link
element) so non-link cards do not show a pointer cursor while linked cards
retain it.
- Around line 53-97: The SVG uses a hardcoded id="skeleton-pulse" which can
collide when multiple MiniChart instances render; change the component (e.g.,
MiniChart in routes/index.tsx) to generate a unique gradient id (use React's
useId or a uniqueId helper) and substitute that id for the linearGradient id and
the polygon fill url reference (and any other url(#...) references) so each
instance gets its own gradient id.
- Around line 245-248: streamsChartData is using the current streams.length for
every metricsHistory point, producing a flat line; change the mapping to read
the historical stream count from metricsHistory (e.g., use m.streams_count or
the appropriate property on each metrics entry) similar to how
functionsChartData and triggersChartData use m.functions_count and
m.triggers_count, and fall back to streams.filter(s => !s.internal).length only
if the historical value is undefined.
In `@packages/console-frontend/src/routes/triggers.tsx`:
- Around line 158-162: The copyToClipboard function calls
navigator.clipboard.writeText without handling rejections; update
copyToClipboard to await or attach a .catch() to navigator.clipboard.writeText
so any errors are handled, only call setCopied(key) on success, and on failure
log the error (or show a user-facing notification) and avoid calling setCopied;
reference the copyToClipboard function and navigator.clipboard.writeText when
making the change.
In `@packages/console-rust/src/bridge/functions.rs`:
- Around line 579-609: The handle_invoke function accepts an unvalidated
function_id string; add format validation similar to validate_flow_id to reject
malformed IDs before calling bridge.call_with_timeout. Modify handle_invoke to
call an existing validate_flow_id-like helper (or add validate_function_id) on
the extracted function_id and return error_response(IIIError::Handler(...)) when
validation fails, and only proceed to call
bridge.call_with_timeout(&function_id, ...) when the ID passes validation;
reference the function name handle_invoke, the variable function_id, and the
call bridge.call_with_timeout in your changes.
| export const Route = createFileRoute('/functions')({ | ||
| component: FunctionsPage, | ||
| loader: ({ context: { queryClient } }) => { | ||
| Promise.allSettled([ | ||
| queryClient.prefetchQuery(functionsQuery()), | ||
| queryClient.prefetchQuery(workersQuery), | ||
| ]) | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Loader should return the promise for proper data prefetching.
Same issue as in triggers.tsx - the loader doesn't return the promise, so TanStack Router won't await the prefetch.
🔧 Proposed fix
loader: ({ context: { queryClient } }) => {
- Promise.allSettled([
+ return Promise.allSettled([
queryClient.prefetchQuery(functionsQuery()),
queryClient.prefetchQuery(workersQuery),
])
},📝 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.
| export const Route = createFileRoute('/functions')({ | |
| component: FunctionsPage, | |
| loader: ({ context: { queryClient } }) => { | |
| Promise.allSettled([ | |
| queryClient.prefetchQuery(functionsQuery()), | |
| queryClient.prefetchQuery(workersQuery), | |
| ]) | |
| }, | |
| }) | |
| export const Route = createFileRoute('/functions')({ | |
| component: FunctionsPage, | |
| loader: ({ context: { queryClient } }) => { | |
| return Promise.allSettled([ | |
| queryClient.prefetchQuery(functionsQuery()), | |
| queryClient.prefetchQuery(workersQuery), | |
| ]) | |
| }, | |
| }) |
🤖 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 26 - 34, The
loader passed to createFileRoute for Route (component: FunctionsPage) starts
prefetching with Promise.allSettled([...]) but doesn't return that promise, so
the router won't await it; update the loader to return the
Promise.allSettled(...) result (i.e., return Promise.allSettled([
queryClient.prefetchQuery(functionsQuery()),
queryClient.prefetchQuery(workersQuery) ])) so TanStack Router will await the
prefetches.
| export const Route = createFileRoute('/triggers')({ | ||
| component: TriggersPage, | ||
| loader: ({ context: { queryClient } }) => { | ||
| Promise.allSettled([ | ||
| queryClient.prefetchQuery(triggersQuery()), | ||
| queryClient.prefetchQuery(functionsQuery()), | ||
| ]) | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Loader should return the promise for proper data prefetching.
The loader calls Promise.allSettled but doesn't return it. While the queries will still fire, TanStack Router won't wait for them before rendering, potentially causing a flash of loading state.
🔧 Proposed fix
loader: ({ context: { queryClient } }) => {
- Promise.allSettled([
+ return Promise.allSettled([
queryClient.prefetchQuery(triggersQuery()),
queryClient.prefetchQuery(functionsQuery()),
])
},📝 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.
| export const Route = createFileRoute('/triggers')({ | |
| component: TriggersPage, | |
| loader: ({ context: { queryClient } }) => { | |
| Promise.allSettled([ | |
| queryClient.prefetchQuery(triggersQuery()), | |
| queryClient.prefetchQuery(functionsQuery()), | |
| ]) | |
| }, | |
| }) | |
| export const Route = createFileRoute('/triggers')({ | |
| component: TriggersPage, | |
| loader: ({ context: { queryClient } }) => { | |
| return Promise.allSettled([ | |
| queryClient.prefetchQuery(triggersQuery()), | |
| queryClient.prefetchQuery(functionsQuery()), | |
| ]) | |
| }, | |
| }) |
🤖 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 30 - 38, The
loader for Route invokes Promise.allSettled([...]) but doesn't return it, so
TanStack Router won't wait for queryClient.prefetchQuery(triggersQuery()) and
queryClient.prefetchQuery(functionsQuery()) to complete; fix by returning the
Promise from the loader (e.g., add a return before Promise.allSettled or make
the loader async and return/await the Promise.allSettled result) so Route.loader
returns the prefetch promise and the router will await the queries.
feat: refactor routes and components for functions and triggers
Summary
Splits the combined Handlers page into dedicated Functions and Triggers routes, improves the Sidebar with an engine health popover, and updates navigation and route structure across the console.
Key Changes
handlers.tsx→functions.tsx+triggers.tsx/handlersreplaced by/functionsand/triggershealthQuery) instead of manual fetch polling/handlersto/functions; Triggers addedType of Change
Checklist
Additional Context
Files changed (15):
Sidebar.tsx— Health popover, nav items, TanStack Queryfunctions.tsx— New route (from handlers)triggers.tsx— New route (from handlers)handlers.tsx— Removedindex.tsx,routeTree.gen.ts,config.tsx,logs.tsx,states.tsx,streams.tsx,traces.tsx— Route/config updatesglobals.css— Minor style additionbridge/functions.rs,bridge/triggers.rs— Rust bridge updatesSummary by CodeRabbit
New Features
Navigation Changes
UI Improvements