feat(explorer): add solvers page and integrate solvers info fetching#7037
feat(explorer): add solvers page and integrate solvers info fetching#7037fairlighteth merged 28 commits intodevelopfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a new Solvers feature: menu entry and route, lazily-loaded Solvers page, directory UI (table, rows, filters, styles), CMS-backed fetcher with types and caching, hook refactor returning loading/error, tests, and a small package dependency. Changes
Sequence DiagramsequenceDiagram
participant User
participant Browser as SolversPage (Browser)
participant Hook as useSolversInfo
participant Utils as fetchSolversInfo
participant CMS as Backend/CMS API
User->>Browser: navigate to /solvers
activate Browser
Browser->>Hook: call useSolversInfo(network?)
activate Hook
Hook->>Hook: set isLoading=true, error=null
Hook->>Utils: fetchSolversInfo(network?)
activate Utils
Utils->>CMS: GET /solvers
activate CMS
CMS-->>Utils: return solver data
deactivate CMS
Utils-->>Hook: return SolversInfo (cached/filtered)
deactivate Utils
Hook->>Hook: set solversInfo, isLoading=false
Hook-->>Browser: return { solversInfo, isLoading, error }
deactivate Hook
Browser->>Browser: render snapshot iframe + directory table
User->>Browser: apply search/filters/expand rows
Browser->>Browser: local filtering + UI update
deactivate Browser
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
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)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (11)
apps/explorer/src/utils/fetchSolversInfo.ts (1)
111-114: Prefer explicitnetwork === undefinedover falsy!network
!networkistruefor0,NaN, andnullin addition toundefined. Although chain ID0is not a real value today, the intent is clearer and safer with an explicit check.✏️ Proposed fix
- if (!network) { + if (network === undefined) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/utils/fetchSolversInfo.ts` around lines 111 - 114, The guard in filterSolversByNetwork currently uses a falsy check (!network) which treats 0, NaN, and null as "no network"; update the check to explicitly test for undefined (network === undefined) so only an omitted network returns allSolvers. Locate function filterSolversByNetwork and replace the early-return condition to use network === undefined, leaving the rest of the logic unchanged.apps/explorer/src/explorer/pages/SolversDirectoryTable.tsx (1)
40-43:buildBodyRowsis a JSX-returning render factory — extract it as a componentWrapping a JSX-producing helper in
useMemodoesn't make it a component; React cannot track component identity, reconcile state, or manage lifecycle for nodes produced this way. Per coding guidelines: "Never declare components inside render bodies or rely onrender*/get*helpers that return JSX; hoist subcomponents to module scope."Extract
buildBodyRowsinto a proper<SolversDirectoryTableBody>component that receives the same parameters as props:- const body = useMemo( - () => buildBodyRows(filteredSolvers, expandedRows, networkFilter, environmentFilter, toggleExpandedRow), - [environmentFilter, expandedRows, filteredSolvers, networkFilter, toggleExpandedRow], - )+// module scope (or separate file) +interface TableBodyProps { + solvers: SolverInfo[] + expandedRows: Record<string, boolean> + networkFilter: string + environmentFilter: string + onToggle: (solverId: string) => void +} +const SolversDirectoryTableBody = React.memo(function SolversDirectoryTableBody({ + solvers, expandedRows, networkFilter, environmentFilter, onToggle, +}: TableBodyProps): React.ReactNode { /* ... */ })Then render it directly in JSX:
+ <SolversDirectoryTableBody + solvers={filteredSolvers} + expandedRows={expandedRows} + networkFilter={networkFilter} + environmentFilter={environmentFilter} + onToggle={toggleExpandedRow} + />As per coding guidelines: "Never use inline render factories to dodge
react/no-unstable-nested-componentswarnings; extract components instead."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.tsx` around lines 40 - 43, The current useMemo wraps a JSX-producing helper buildBodyRows inside SolversDirectoryTable, which prevents React from treating it as a proper component; extract buildBodyRows into a module-level React component named SolversDirectoryTableBody that accepts props (filteredSolvers, expandedRows, networkFilter, environmentFilter, toggleExpandedRow), move any logic from buildBodyRows into that component (and keep only pure prop/state usage), remove the useMemo call in SolversDirectoryTable, and render <SolversDirectoryTableBody ...props /> directly in the JSX so React can reconcile, manage lifecycle, and satisfy react/no-unstable-nested-components rules.apps/explorer/src/hooks/useSolversInfo.ts (2)
47-47: Return object must be wrapped inuseMemo
{ solversInfo, isLoading, error }is a new object reference on every render. Per coding guidelines, hooks returning objects must useuseMemo. Any consumer that places the return value in auseEffect/useMemodependency array, or a child wrapped inReact.memo, will re-run unnecessarily.♻️ Proposed fix
- return { solversInfo, isLoading, error } + return useMemo(() => ({ solversInfo, isLoading, error }), [solversInfo, isLoading, error])Also export the type so consumers can annotate variables explicitly:
-type UseSolversInfo = { +export type UseSolversInfo = {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/hooks/useSolversInfo.ts` at line 47, The hook useSolversInfo currently returns a fresh object literal { solversInfo, isLoading, error } on every render which breaks referential stability for consumers; wrap that return value in useMemo to memoize the object (e.g. return useMemo(() => ({ solversInfo, isLoading, error }), [solversInfo, isLoading, error])) so the reference only changes when inputs change, and export a corresponding type (e.g. SolversInfoHookReturn) for consumers to import and annotate variables explicitly; update the export list to include that type.
13-47: New data fetching should use JotaiatomWithQueryinstead ofuseState+useEffectPer coding guidelines: "New data fetching must use Jotai
atomWithQuery(jotai/query); SWR is deprecated." The currentuseState+useEffectpattern duplicates fetch lifecycle management thatatomWithQueryhandles automatically (loading/error state, deduplication, cache). Migrating to an atom also removes the need for theisSubscribedcleanup flag and the manualEMPTY_SOLVERS_INFOsentinel.Based on learnings: "New data fetching must use Jotai
atomWithQuery(jotai/query); SWR is deprecated."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/hooks/useSolversInfo.ts` around lines 13 - 47, Replace the manual useState/useEffect fetch in useSolversInfo with a Jotai atomWithQuery: create an atom (e.g., solversInfoQueryAtom) that calls fetchSolversInfo(network) and exposes data/loading/error, remove isSubscribed logic and EMPTY_SOLVERS_INFO usage, then have useSolversInfo read that atom (useAtomValue/useAtom) and return { solversInfo: data ?? undefined, isLoading: isLoading, error }; ensure the atom key depends on the network parameter so queries are cached/deduped per network and reference the existing fetchSolversInfo, UseSolversInfo type, and EMPTY_SOLVERS_INFO concept when mapping fallback data.apps/explorer/src/explorer/pages/Solvers.tsx (1)
52-52: Extract the hardcoded Dune embed URL to a named constantThe magic string
"https://dune.com/embeds/5931238/9574995"will silently become stale if the embed changes. Hoisting it to a named constant (e.g.,SOLVERS_DUNE_EMBED_URL) inconst.tsor at the top of this file makes future updates discoverable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/explorer/pages/Solvers.tsx` at line 52, The hardcoded Dune embed URL in the Solvers component should be moved to a named constant so it’s discoverable and easy to update: create a constant (e.g., SOLVERS_DUNE_EMBED_URL) either in this file or in a shared consts module (const.ts) and replace the inline string "https://dune.com/embeds/5931238/9574995" in Solvers.tsx with that constant; update any imports if you place it in const.ts and ensure the iframe/src or embed prop references SOLVERS_DUNE_EMBED_URL.apps/explorer/src/explorer/pages/SolversDirectoryTable.styles.tsx (2)
171-188: Duplicated grid-template-columns value.The grid template
12rem 8rem 1fr 1fr 6remis repeated in bothDeploymentsGridHeaderandDeploymentsGridRow. Extract it into a shared constant to keep them in sync.Suggested fix
+const DEPLOYMENTS_GRID_COLUMNS = '12rem 8rem 1fr 1fr 6rem' + export const DeploymentsGridHeader = styled.div` display: grid; - grid-template-columns: 12rem 8rem 1fr 1fr 6rem; + grid-template-columns: ${DEPLOYMENTS_GRID_COLUMNS}; gap: 0.8rem; color: ${Color.explorer_textSecondary2}; font-size: 1.1rem; margin-bottom: 0.8rem; ` export const DeploymentsGridRow = styled.div` display: grid; - grid-template-columns: 12rem 8rem 1fr 1fr 6rem; + grid-template-columns: ${DEPLOYMENTS_GRID_COLUMNS}; gap: 0.8rem; padding: 0.6rem 0; border-top: 0.1rem solid ${Color.explorer_border}; align-items: center; font-size: 1.2rem; `As per coding guidelines: "Hoist repeating strings/tooltips into constants colocated with the feature."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.styles.tsx` around lines 171 - 188, The grid-template-columns value "12rem 8rem 1fr 1fr 6rem" is duplicated in DeploymentsGridHeader and DeploymentsGridRow; hoist it into a single constant (e.g., deploymentsGridTemplateColumns) colocated in this file and replace the literal in both styled components to reference that constant so the layout stays in sync.
139-150:EnvTagcolor logic assumes only two environments.The ternary maps
'prod'to green and everything else to orange. This is reasonable for the current prod/barn model, but if additional environments are introduced, they'll all appear as orange. Consider using anenum + Record<Enum, T>pattern or at least adding a brief comment noting the intentional fallback.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.styles.tsx` around lines 139 - 150, EnvTag's color logic currently treats only 'prod' as green and everything else as orange, which will mis-color any new environments; update the implementation to map environments to colors using a clear mapping (e.g., an Environment enum or union type plus a Record<Environment, { color, background, border }>) and have EnvTag read values from that map instead of a ternary, or at minimum add a comment above EnvTag explaining the deliberate fallback for non-prod environments; reference the EnvTag styled component and the $environment prop when making the change.apps/explorer/src/explorer/pages/Solvers.styles.tsx (2)
92-109: Rawrgbavalue instead of aColortoken.Line 94 uses
rgba(255, 255, 255, 0.06)while the rest of the file consistently usesColor.*tokens. If a suitable token exists (e.g.,Color.explorer_borderor similar), prefer it for theming consistency.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/explorer/pages/Solvers.styles.tsx` around lines 92 - 109, The ChartWrapper styled component uses a raw rgba value for the border; replace that hardcoded "rgba(255, 255, 255, 0.06)" with the appropriate theme Color token (e.g., Color.explorer_border or the matching token used elsewhere) in the border property of ChartWrapper, and update imports if necessary to pull in Color; if no token exists add a new Color token with that rgba value to the theme tokens and use it here to keep theming consistent.
34-37: Inconsistent unit:16pxvsrem.Every other font-size in both style files uses
remunits (e.g.,1.2rem,1.3rem,2rem). Line 36 uses16px. For consistency, use1.6reminstead.Suggested fix
export const SectionTitleMeta = styled.span` color: ${Color.explorer_textSecondary2}; - font-size: 16px; + font-size: 1.6rem; `🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/explorer/pages/Solvers.styles.tsx` around lines 34 - 37, The SectionTitleMeta styled component uses an inconsistent unit for font-size ("16px"); update SectionTitleMeta (styled.span) to use rem units to match the rest of the file—change font-size from 16px to 1.6rem so it follows the existing rem-based sizing convention.apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx (2)
1-220: File is 220 LOC — consider splitting rendering and filtering concerns.The guideline asks for ~200 LOC per file with justification needed above that. This file mixes pure filtering/data logic (
matchesSearch,filterSolvers,filterDeployments,getNetworkOptions,getEnvironmentOptions) with rendering logic (renderSummaryRow,buildBodyRows, etc.). Splitting these into separate modules (e.g.,SolversDirectoryTable.filters.tsfor pure logic, keeping rendering helpers in the current file) would bring both under the limit and improve cohesion.As per coding guidelines: "Keep TypeScript/TSX sources around 200 LOC; anything over 200 needs active justification."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around lines 1 - 220, This file mixes pure data/filter logic with rendering and exceeds the 200 LOC guideline; extract the pure functions matchesSearch, filterDeployments, getNetworkOptions, getEnvironmentOptions, and filterSolvers into a new module (e.g., SolversDirectoryTable.filters.ts) and export them, leaving only rendering helpers (renderSolverIcon, renderNetworkChips, renderEnvironmentTags, renderSummaryRow, renderDetailsRow, buildBodyRows) in the original file; update imports/exports so the renderer file imports the moved functions, keep all type references (SolverInfo, SolverDeployment) intact, and run a quick compile to adjust any named imports/exports.
31-33: CastchainId as SupportedChainIdmay silently returnundefined.If a CMS-sourced
chainIddoesn't exist inCHAIN_INFO, the optional chain?.logo?.lightsafely returnsundefined, so this won't crash. However, the cast bypasses type safety. A runtime guard (e.g., checkingchainId in CHAIN_INFO) would be more explicit and avoid the cast entirely.Suggested approach
function getChainIcon(chainId: number): string | undefined { - return CHAIN_INFO[chainId as SupportedChainId]?.logo?.light || undefined + const info = chainId in CHAIN_INFO ? CHAIN_INFO[chainId as SupportedChainId] : undefined + return info?.logo?.light || undefined }As per coding guidelines: "Double-check casts, especially across chains and bridge flows."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around lines 31 - 33, The getChainIcon function currently casts chainId as SupportedChainId which bypasses type safety; replace the cast with an explicit runtime guard against CHAIN_INFO (e.g., check that chainId exists in CHAIN_INFO or that CHAIN_INFO[chainId] is defined) and then return CHAIN_INFO[chainId].logo?.light or undefined accordingly so you remove the unsafe cast of SupportedChainId and preserve the existing optional chaining behavior; reference function getChainIcon and constant CHAIN_INFO.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/explorer/src/explorer/pages/Solvers.tsx`:
- Around line 51-57: Add a sandbox attribute to the Dune iframe to restrict
capabilities: update the iframe element in Solvers.tsx (the iframe with src
"https://dune.com/embeds/5931238/9574995" and title "Solvers across networks")
to include sandbox="allow-scripts allow-same-origin" while keeping existing
attributes (loading, referrerPolicy, allow) unchanged so the chart still renders
but the frame gains defense-in-depth restrictions.
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 35-38: Convert the helper functions that return JSX into top-level
React function components: replace renderSolverIcon with a SolverIcon component
that accepts a solver prop (used as <SolverIcon solver={solver} />), convert
renderNetworkChips -> NetworkChips, renderEnvironmentTags -> EnvironmentTags,
renderSummaryRow -> SummaryRow, and renderDetailsRow -> DetailsRow; hoist each
new component to module scope (outside any render bodies), accept the same data
props the helper previously took, return identical JSX, and update all call
sites to use the new JSX component form so React can properly reconcile and
allow future memoization (e.g., React.memo) if desired.
In `@apps/explorer/src/utils/fetchSolversInfo.ts`:
- Line 20: The module-level new URL(CMS_BASE_URL).origin (CMS_ORIGIN) can throw
during import if the env var is malformed; update fetchSolversInfo.ts to guard
this by wrapping the URL parse in a try/catch (or a small init function) that
catches TypeError and falls back to a safe value (e.g., empty string or
window.location.origin) and logs or warns about the malformed CMS_BASE_URL, then
use that safe CMS_ORIGIN everywhere; ensure you reference and replace the
module-level constant CMS_ORIGIN with the new guarded value or accessor so
imports won't crash on invalid env values.
- Around line 116-127: filterSolversByNetwork currently keeps solvers if
solver.deployments.length > 0 even when all those deployments yield no networks
(e.g., all are inactive), breaking the invariant that solvers have networks; fix
by computing the networks for the per-chain deployments using
mapSolverNetworks(deployments) and only returning the solver when
networks.length > 0, returning the same deployments and networks values (i.e.,
replace the final .filter((solver) => solver.deployments.length > 0) with a
filter that requires mapSolverNetworks(deployments).length > 0 or compute const
networks = mapSolverNetworks(deployments) and filter on networks.length > 0
before returning { ...solver, deployments, networks } so inactive-only
deployments are excluded and the networks invariant is preserved.
---
Nitpick comments:
In `@apps/explorer/src/explorer/pages/Solvers.styles.tsx`:
- Around line 92-109: The ChartWrapper styled component uses a raw rgba value
for the border; replace that hardcoded "rgba(255, 255, 255, 0.06)" with the
appropriate theme Color token (e.g., Color.explorer_border or the matching token
used elsewhere) in the border property of ChartWrapper, and update imports if
necessary to pull in Color; if no token exists add a new Color token with that
rgba value to the theme tokens and use it here to keep theming consistent.
- Around line 34-37: The SectionTitleMeta styled component uses an inconsistent
unit for font-size ("16px"); update SectionTitleMeta (styled.span) to use rem
units to match the rest of the file—change font-size from 16px to 1.6rem so it
follows the existing rem-based sizing convention.
In `@apps/explorer/src/explorer/pages/Solvers.tsx`:
- Line 52: The hardcoded Dune embed URL in the Solvers component should be moved
to a named constant so it’s discoverable and easy to update: create a constant
(e.g., SOLVERS_DUNE_EMBED_URL) either in this file or in a shared consts module
(const.ts) and replace the inline string
"https://dune.com/embeds/5931238/9574995" in Solvers.tsx with that constant;
update any imports if you place it in const.ts and ensure the iframe/src or
embed prop references SOLVERS_DUNE_EMBED_URL.
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 1-220: This file mixes pure data/filter logic with rendering and
exceeds the 200 LOC guideline; extract the pure functions matchesSearch,
filterDeployments, getNetworkOptions, getEnvironmentOptions, and filterSolvers
into a new module (e.g., SolversDirectoryTable.filters.ts) and export them,
leaving only rendering helpers (renderSolverIcon, renderNetworkChips,
renderEnvironmentTags, renderSummaryRow, renderDetailsRow, buildBodyRows) in the
original file; update imports/exports so the renderer file imports the moved
functions, keep all type references (SolverInfo, SolverDeployment) intact, and
run a quick compile to adjust any named imports/exports.
- Around line 31-33: The getChainIcon function currently casts chainId as
SupportedChainId which bypasses type safety; replace the cast with an explicit
runtime guard against CHAIN_INFO (e.g., check that chainId exists in CHAIN_INFO
or that CHAIN_INFO[chainId] is defined) and then return
CHAIN_INFO[chainId].logo?.light or undefined accordingly so you remove the
unsafe cast of SupportedChainId and preserve the existing optional chaining
behavior; reference function getChainIcon and constant CHAIN_INFO.
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.styles.tsx`:
- Around line 171-188: The grid-template-columns value "12rem 8rem 1fr 1fr 6rem"
is duplicated in DeploymentsGridHeader and DeploymentsGridRow; hoist it into a
single constant (e.g., deploymentsGridTemplateColumns) colocated in this file
and replace the literal in both styled components to reference that constant so
the layout stays in sync.
- Around line 139-150: EnvTag's color logic currently treats only 'prod' as
green and everything else as orange, which will mis-color any new environments;
update the implementation to map environments to colors using a clear mapping
(e.g., an Environment enum or union type plus a Record<Environment, { color,
background, border }>) and have EnvTag read values from that map instead of a
ternary, or at minimum add a comment above EnvTag explaining the deliberate
fallback for non-prod environments; reference the EnvTag styled component and
the $environment prop when making the change.
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.tsx`:
- Around line 40-43: The current useMemo wraps a JSX-producing helper
buildBodyRows inside SolversDirectoryTable, which prevents React from treating
it as a proper component; extract buildBodyRows into a module-level React
component named SolversDirectoryTableBody that accepts props (filteredSolvers,
expandedRows, networkFilter, environmentFilter, toggleExpandedRow), move any
logic from buildBodyRows into that component (and keep only pure prop/state
usage), remove the useMemo call in SolversDirectoryTable, and render
<SolversDirectoryTableBody ...props /> directly in the JSX so React can
reconcile, manage lifecycle, and satisfy react/no-unstable-nested-components
rules.
In `@apps/explorer/src/hooks/useSolversInfo.ts`:
- Line 47: The hook useSolversInfo currently returns a fresh object literal {
solversInfo, isLoading, error } on every render which breaks referential
stability for consumers; wrap that return value in useMemo to memoize the object
(e.g. return useMemo(() => ({ solversInfo, isLoading, error }), [solversInfo,
isLoading, error])) so the reference only changes when inputs change, and export
a corresponding type (e.g. SolversInfoHookReturn) for consumers to import and
annotate variables explicitly; update the export list to include that type.
- Around line 13-47: Replace the manual useState/useEffect fetch in
useSolversInfo with a Jotai atomWithQuery: create an atom (e.g.,
solversInfoQueryAtom) that calls fetchSolversInfo(network) and exposes
data/loading/error, remove isSubscribed logic and EMPTY_SOLVERS_INFO usage, then
have useSolversInfo read that atom (useAtomValue/useAtom) and return {
solversInfo: data ?? undefined, isLoading: isLoading, error }; ensure the atom
key depends on the network parameter so queries are cached/deduped per network
and reference the existing fetchSolversInfo, UseSolversInfo type, and
EMPTY_SOLVERS_INFO concept when mapping fallback data.
In `@apps/explorer/src/utils/fetchSolversInfo.ts`:
- Around line 111-114: The guard in filterSolversByNetwork currently uses a
falsy check (!network) which treats 0, NaN, and null as "no network"; update the
check to explicitly test for undefined (network === undefined) so only an
omitted network returns allSolvers. Locate function filterSolversByNetwork and
replace the early-return condition to use network === undefined, leaving the
rest of the logic unchanged.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx (1)
32-34:|| undefinedingetChainIconis redundant.Optional chaining already returns
undefinedwhen any link in the chain is nullish. The|| undefinedonly adds noise (and would silently suppress an unexpected empty-string URL). Remove it or, if guarding empty strings is intentional, use|| undefinedwith a comment.♻️ Proposed fix
- return CHAIN_INFO[chainId as SupportedChainId]?.logo?.light || undefined + return CHAIN_INFO[chainId as SupportedChainId]?.logo?.light🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around lines 32 - 34, The getChainIcon function uses optional chaining on CHAIN_INFO[chainId as SupportedChainId]?.logo?.light so the trailing "|| undefined" is redundant and should be removed; update the getChainIcon implementation to simply return CHAIN_INFO[chainId as SupportedChainId]?.logo?.light, or if you intentionally want to guard against empty-string URLs, replace the "|| undefined" with an explicit check (e.g., return value || undefined) and add a short comment explaining the empty-string guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 140-142: Update the ExpandButton to expose its state and target to
assistive tech: set aria-expanded to the boolean isExpanded and change the
aria-label to a descriptive string that includes the target solver (e.g.,
reference solver.solverId or solver.name) so the button reads like "Toggle
deployments for {solver.solverId}"; keep the onClick handler (onToggle)
unchanged.
- Around line 214-238: Replace the render-factory function buildBodyRows with a
proper React component named SolverTableBody that accepts props (solvers,
expandedRows, networkFilter, environmentFilter, onToggle) and returns the same
JSX; hoist and convert the helper renderSummaryRow and renderDetailsRow into
module-scope components (e.g., SummaryRow, DetailsRow) and use them inside
SolverTableBody so reconciliation is explicit and no JSX-returning helpers are
used inside render paths; update the calling component to render
<SolverTableBody .../> with the same props and preserve existing behavior for
empty solvers and expandedRows logic.
---
Duplicate comments:
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 36-39: The file still uses plain functions that return JSX —
renderSolverIcon, renderNetworkChips, renderEnvironmentTags, renderSummaryRow,
and renderDetailsRow — which must be converted into module‑scope React function
components (e.g., SolverIcon, NetworkChips, EnvironmentTags, SummaryRow,
DetailsRow) so React can properly reconcile and you can apply memoization;
replace calls to the render* helpers with JSX usage of the new components,
accept the same props (SolverInfo, networks/env/summar y props) as parameters,
move them to top-level module scope, and wrap with React.memo where appropriate
(for example SolverIcon and NetworkChips) to preserve performance semantics.
---
Nitpick comments:
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx`:
- Around line 32-34: The getChainIcon function uses optional chaining on
CHAIN_INFO[chainId as SupportedChainId]?.logo?.light so the trailing "||
undefined" is redundant and should be removed; update the getChainIcon
implementation to simply return CHAIN_INFO[chainId as
SupportedChainId]?.logo?.light, or if you intentionally want to guard against
empty-string URLs, replace the "|| undefined" with an explicit check (e.g.,
return value || undefined) and add a short comment explaining the empty-string
guard.
| <ExpandButton onClick={(): void => onToggle(solver.solverId)} aria-label="Toggle deployments"> | ||
| {isExpanded ? '▾' : '▸'} | ||
| </ExpandButton> |
There was a problem hiding this comment.
ExpandButton is missing aria-expanded and has a non-descriptive aria-label.
aria-label="Toggle deployments"gives no context about which solver is being toggled.- The expanded/collapsed state is conveyed only visually (
▾/▸);aria-expandedis absent, so assistive technologies cannot report the current state.
🛡️ Proposed fix
-<ExpandButton onClick={(): void => onToggle(solver.solverId)} aria-label="Toggle deployments">
- {isExpanded ? '▾' : '▸'}
+<ExpandButton
+ onClick={(): void => onToggle(solver.solverId)}
+ aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${solver.displayName} deployments`}
+ aria-expanded={isExpanded}
+>
+ {isExpanded ? '▾' : '▸'}📝 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.
| <ExpandButton onClick={(): void => onToggle(solver.solverId)} aria-label="Toggle deployments"> | |
| {isExpanded ? '▾' : '▸'} | |
| </ExpandButton> | |
| <ExpandButton | |
| onClick={(): void => onToggle(solver.solverId)} | |
| aria-label={`${isExpanded ? 'Collapse' : 'Expand'} ${solver.displayName} deployments`} | |
| aria-expanded={isExpanded} | |
| > | |
| {isExpanded ? '▾' : '▸'} | |
| </ExpandButton> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around
lines 140 - 142, Update the ExpandButton to expose its state and target to
assistive tech: set aria-expanded to the boolean isExpanded and change the
aria-label to a descriptive string that includes the target solver (e.g.,
reference solver.solverId or solver.name) so the button reads like "Toggle
deployments for {solver.solverId}"; keep the onClick handler (onToggle)
unchanged.
| export function buildBodyRows( | ||
| solvers: SolverInfo[], | ||
| expandedRows: Record<string, boolean>, | ||
| networkFilter: string, | ||
| environmentFilter: string, | ||
| onToggle: (solverId: string) => void, | ||
| ): React.ReactNode { | ||
| if (!solvers.length) { | ||
| return ( | ||
| <tr> | ||
| <td colSpan={5}> | ||
| <Placeholder>No solvers match your current filters.</Placeholder> | ||
| </td> | ||
| </tr> | ||
| ) | ||
| } | ||
|
|
||
| return solvers.flatMap((solver) => { | ||
| const isExpanded = !!expandedRows[solver.solverId] | ||
| const deployments = filterDeployments(solver.deployments, networkFilter, environmentFilter) | ||
| const summary = renderSummaryRow(solver, isExpanded, onToggle) | ||
|
|
||
| if (!isExpanded) return [summary] | ||
| return [summary, renderDetailsRow(solver, deployments)] | ||
| }) |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
buildBodyRows is itself a JSX-returning render factory and should be a proper component.
Even though it doesn't have the render* prefix, buildBodyRows is invoked inside a component's JSX return and produces React nodes — the same render-factory anti-pattern the guidelines prohibit. It should be converted to a proper <SolverTableBody> component that accepts the same parameters as props:
♻️ Proposed refactor
-export function buildBodyRows(
- solvers: SolverInfo[],
- expandedRows: Record<string, boolean>,
- networkFilter: string,
- environmentFilter: string,
- onToggle: (solverId: string) => void,
-): React.ReactNode {
- if (!solvers.length) {
- return (
- <tr>
- <td colSpan={5}>
- <Placeholder>No solvers match your current filters.</Placeholder>
- </td>
- </tr>
- )
- }
-
- return solvers.flatMap((solver) => {
- const isExpanded = !!expandedRows[solver.solverId]
- const deployments = filterDeployments(solver.deployments, networkFilter, environmentFilter)
- const summary = renderSummaryRow(solver, isExpanded, onToggle)
- if (!isExpanded) return [summary]
- return [summary, renderDetailsRow(solver, deployments)]
- })
-}
+interface SolverTableBodyProps {
+ solvers: SolverInfo[]
+ expandedRows: Record<string, boolean>
+ networkFilter: string
+ environmentFilter: string
+ onToggle: (solverId: string) => void
+}
+
+export function SolverTableBody({
+ solvers,
+ expandedRows,
+ networkFilter,
+ environmentFilter,
+ onToggle,
+}: SolverTableBodyProps): React.ReactNode {
+ if (!solvers.length) {
+ return (
+ <tr>
+ <td colSpan={5}>
+ <Placeholder>No solvers match your current filters.</Placeholder>
+ </td>
+ </tr>
+ )
+ }
+
+ return solvers.flatMap((solver) => {
+ const isExpanded = !!expandedRows[solver.solverId]
+ const deployments = filterDeployments(solver.deployments, networkFilter, environmentFilter)
+ return isExpanded
+ ? [<SummaryRow key={solver.solverId} solver={solver} isExpanded onToggle={onToggle} />,
+ <DetailsRow key={`${solver.solverId}-details`} solver={solver} deployments={deployments} />]
+ : [<SummaryRow key={solver.solverId} solver={solver} isExpanded={false} onToggle={onToggle} />]
+ })
+}Combined with converting the private render* helpers into named components (SummaryRow, DetailsRow, etc.), this brings the module well within the 200-LOC target and makes React reconciliation fully explicit.
As per coding guidelines: "Never declare components inside render bodies or rely on render*/get* helpers that return JSX; hoist subcomponents to module scope."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/explorer/src/explorer/pages/SolversDirectoryTable.helpers.tsx` around
lines 214 - 238, Replace the render-factory function buildBodyRows with a proper
React component named SolverTableBody that accepts props (solvers, expandedRows,
networkFilter, environmentFilter, onToggle) and returns the same JSX; hoist and
convert the helper renderSummaryRow and renderDetailsRow into module-scope
components (e.g., SummaryRow, DetailsRow) and use them inside SolverTableBody so
reconciliation is explicit and no JSX-returning helpers are used inside render
paths; update the calling component to render <SolverTableBody .../> with the
same props and preserve existing behavior for empty solvers and expandedRows
logic.
There was a problem hiding this comment.
Hey @fairlighteth , cool, great job!
As usual. some issues-questions from my side:
- Page icon in the menu: we use balck-white everywhere else in the menu, but this one is green. Looks weird, frankly
2. Might it be possible to add navigation links to block explorers to Solver addresses? If it is too complicated, add a copy icon to these addresses.. WDYT? This must have been fixed while I was writing my comment.
- Might be not important, but 'lens' chain icon is not visible
- I see that in some chains INK solver details are duplicated. Could you please check why?
- For some solvers like '1Inch, Baseline, Quasimodo Webside navigates to Gnosis.io. Is it correct info?
- The page is closed when I change a chain in the network selecotor. I think, it should not depend of a chain selected there. Might it be possible not to navigate a user to the Home page when another chain is picked?
- It would be great to improve responsive views:
-
WDYT about adding a link to this page to the CoW Swap/Cow'fi footer? Or to the menu there? :)
-
I noticed, that Ink chain is missing in the chart. Is it OK?
-
WDYT about adding one more filter: 'Active: Yes/No"?
-
Search field: It would be great to add a cross icon into there to reset a search value :)
- What is 'chain 10' for Baseline solver?
- It is weird: Copium solver has a solver address on Barn, but it is filetrerd as running on Prod only. Is it OK?
Thank you!
|
|
||
| export async function fetchSolversInfo(network?: number): Promise<SolversInfo> { | ||
| if (!solversInfoCache) { | ||
| const response = await fetch(SOLVERS_API_URL) |
There was a problem hiding this comment.
Would be great to use import { getCmsClient } from '@cowprotocol/core' instead
| return CHAIN_INFO[chainId as SupportedChainId]?.logo?.light || undefined | ||
| } | ||
|
|
||
| function renderSolverIcon(solver: SolverInfo): React.ReactNode { |
| }) | ||
| } | ||
|
|
||
| function renderNetworkChips(solver: SolverInfo): React.ReactNode { |
| ) | ||
| } | ||
|
|
||
| function renderEnvironmentTags(solver: SolverInfo): React.ReactNode { |
|
|
@shoom3301 addressed all issues |
…d add corresponding tests
elena-zh
left a comment
There was a problem hiding this comment.
Thank you, @fairlighteth , looks good.
As for the cms, should we let the solvers team know to prettify solvers data before releasing these changes?
|
@elena-zh informed the team. Would not make it a blocker for releasing this PR. |
| import { getExplorerBaseUrl } from '@cowprotocol/common-utils' | ||
| import { SupportedChainId } from '@cowprotocol/cow-sdk' | ||
|
|
||
| export function getSolversExplorerUrl(): string { |
There was a problem hiding this comment.
Nitpick, it can be a const
There was a problem hiding this comment.
Addressed now.
| ) | ||
| } | ||
|
|
||
| export function SolverDetailsRow({ solver, deployments }: SolverDetailsRowProps): React.ReactNode { |
There was a problem hiding this comment.
@fairlighteth this file is big enough, you could extract pure components to a separate file
There was a problem hiding this comment.
Addressed now.
…ponents for Solvers Directory
…mmaryRow to use it

Summary
Adds a new public Solvers page in Explorer and wires it into navigation/routes, with CMS-backed solver data and a polished UX for discovery + inspection.
This change includes:
Screenshots:
To Test
Background
Explorer previously had a dormant temporary solver source path and no user-facing solvers directory page. This PR consolidates solver presentation around CMS metadata, keeps a live activity view via
Dune embed, and improves discoverability by merging summary + deployment details into a single expandable directory workflow.
Summary by CodeRabbit
New Features
Improvements
Tests