Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds alias-based search functionality for target nodes in job deployment. Users can now search for nodes by typing an alias (instead of only using node addresses), with a dropdown showing matching active nodes that can be selected.
Changes:
- Added
getActiveNodesAPI wrapper to fetch active nodes with optional alias pattern filtering - Implemented autocomplete dropdown with debounced search when users type node aliases
- Enhanced UX with loading states, error handling, and proper cleanup of async operations
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| src/lib/api/oracles.tsx | Added getActiveNodes function and type definitions for fetching active nodes with alias pattern filtering |
| src/shared/jobs/target-nodes/TargetNodesSection.tsx | Implemented alias search UI with dropdown, state management, debouncing, and integration with existing node validation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export const getActiveNodes = async ( | ||
| page: number = 1, | ||
| pageSize: number = 10, | ||
| aliasPattern?: string, | ||
| options?: RequestOptions, | ||
| ): Promise<ActiveNodesResult> => { | ||
| const searchParams = new URLSearchParams({ | ||
| items_per_page: String(pageSize), | ||
| page: String(page), | ||
| }); | ||
|
|
||
| const normalizedAliasPattern = aliasPattern?.trim(); | ||
| if (normalizedAliasPattern) { | ||
| searchParams.set('alias_pattern', normalizedAliasPattern); | ||
| } | ||
|
|
||
| const response = await fetch(`${oraclesUrl}/active_nodes_list?${searchParams.toString()}`, { | ||
| signal: options?.signal, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error('Failed to fetch active nodes.'); | ||
| } | ||
|
|
||
| const payload: { result: ActiveNodesResult } = await response.json(); | ||
| if (payload?.result?.error) { | ||
| throw new Error(payload.result.error); | ||
| } | ||
|
|
||
| return payload.result; | ||
| }; |
There was a problem hiding this comment.
The getActiveNodes function uses raw fetch while other API functions in this file use axios through _doGet helper. This creates inconsistency in error handling (no toApiError wrapper) and request configuration. Consider using axios with the axiosInstance to maintain consistency with other API functions in this file, which would provide automatic error transformation through the interceptor.
| {showAliasSearchDropdown && ( | ||
| <div className="absolute top-full right-0 left-0 z-20 mt-1 rounded-lg border border-slate-200 bg-white shadow-md"> | ||
| <div className="max-h-52 overflow-y-auto py-1"> | ||
| {isAliasSearchLoading && ( | ||
| <div className="px-3 py-2 text-sm text-slate-500"> | ||
| Searching nodes... | ||
| </div> | ||
| )} | ||
|
|
||
| {!isAliasSearchLoading && aliasSuggestions.length === 0 && ( | ||
| <div className="px-3 py-2 text-sm text-slate-500"> | ||
| No active nodes matched this alias. | ||
| </div> | ||
| )} | ||
|
|
||
| {!isAliasSearchLoading && | ||
| aliasSuggestions.map((suggestion) => ( | ||
| <button | ||
| key={suggestion.address} | ||
| type="button" | ||
| className="w-full cursor-pointer px-3 py-2 text-left hover:bg-slate-50" | ||
| onMouseDown={async (event) => { | ||
| event.preventDefault(); | ||
| clearAliasSearchCloseTimeout(); | ||
| field.onChange(suggestion.address); | ||
| setValue( | ||
| `deployment.targetNodes.${index}.address`, | ||
| suggestion.address, | ||
| ); | ||
| setNodeInfoToIdle(suggestion.address); | ||
| setActiveAliasSearchIndex(null); | ||
| setAliasSuggestions([]); | ||
|
|
||
| await trigger('deployment.targetNodes'); | ||
| await fetchNodeInfoForAddress( | ||
| suggestion.address, | ||
| ); | ||
| }} | ||
| > | ||
| <div className="flex items-center justify-between gap-2"> | ||
| <div className="truncate text-sm font-medium text-slate-700"> | ||
| {suggestion.alias || suggestion.address} | ||
| </div> | ||
| <div | ||
| className={`h-2 w-2 rounded-full ${ | ||
| suggestion.isOnline === null | ||
| ? 'bg-slate-300' | ||
| : suggestion.isOnline | ||
| ? 'bg-green-500' | ||
| : 'bg-yellow-500' | ||
| }`} | ||
| /> | ||
| </div> | ||
| <div className="truncate text-xs text-slate-500"> | ||
| {suggestion.address} | ||
| </div> | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| } | ||
| /> | ||
| )} |
There was a problem hiding this comment.
The alias search dropdown lacks keyboard navigation support. Users cannot navigate through suggestions using arrow keys or select with Enter/Tab keys, which is a standard UX pattern for autocomplete dropdowns. Consider adding keyboard event handlers for arrow keys (up/down to navigate), Enter (to select), and Escape (to close) to improve accessibility and user experience.
| <StyledInput | ||
| placeholder="0xai_ or node alias" | ||
| value={value} | ||
| onFocus={() => { | ||
| clearAliasSearchCloseTimeout(); | ||
| setActiveAliasSearchIndex(index); | ||
| }} | ||
| onChange={(e) => { | ||
| const nextValue = e.target.value; | ||
| field.onChange(nextValue); | ||
| setNodeInfoToIdle(nextValue); | ||
| setActiveAliasSearchIndex(index); | ||
| }} | ||
| onBlur={async () => { | ||
| field.onBlur(); | ||
| scheduleAliasSearchClose(index); | ||
| await trigger('deployment.targetNodes'); | ||
| await fetchNodeInfoForAddress(value); | ||
| }} | ||
| isInvalid={hasError} | ||
| errorMessage={specificError || fieldError || rootError} | ||
| endContent={ | ||
| <div className="flex items-center gap-2"> | ||
| <NodeInfoStatusPopover | ||
| nodeInfoState={nodeInfoState} | ||
| normalizedValue={normalizedValue} | ||
| ariaLabel="Show node info" | ||
| /> | ||
|
|
||
| <button | ||
| type="button" | ||
| className="cursor-pointer hover:opacity-60" | ||
| aria-label="Paste target node address from clipboard" | ||
| onClick={async () => { | ||
| try { | ||
| const clipboardText = await navigator.clipboard.readText(); | ||
| field.onChange(clipboardText); | ||
|
|
||
| setValue( | ||
| `deployment.targetNodes.${index}.address`, | ||
| clipboardText, | ||
| ); | ||
| setNodeInfoToIdle(clipboardText); | ||
| setActiveAliasSearchIndex(null); | ||
| setAliasSuggestions([]); | ||
|
|
||
| await fetchNodeInfoForAddress(clipboardText); | ||
| } catch (error) { | ||
| console.error('Failed to read clipboard:', error); | ||
| toast.error( | ||
| 'Unable to read from clipboard. Please paste the address manually.', | ||
| ); | ||
| } | ||
| }} | ||
| > | ||
| <RiClipboardLine className="text-lg text-slate-600" /> | ||
| </button> | ||
| </div> | ||
| } | ||
| /> |
There was a problem hiding this comment.
The alias search dropdown is missing ARIA attributes for proper screen reader support. The input should have aria-expanded, aria-autocomplete="list", aria-controls pointing to the dropdown ID, and aria-activedescendant for the focused suggestion. The dropdown should have role="listbox" and each suggestion should have role="option". These attributes are essential for users relying on assistive technologies.
| setActiveAliasSearchIndex((previousIndex) => (previousIndex === index ? null : previousIndex)); | ||
| setAliasSuggestions([]); | ||
| setIsAliasSearchLoading(false); | ||
| }, 120); |
There was a problem hiding this comment.
The close timeout of 120ms seems very short and might cause the dropdown to close before users can click on a suggestion, especially on slower devices or for users with reduced dexterity. Consider increasing this to at least 200-300ms to provide a better user experience. Alternatively, you could use onMouseEnter/onMouseLeave on the dropdown container to manage visibility more reliably.
| }, 120); | |
| }, 250); |
| const isAbortError = (error: unknown) => { | ||
| if (!error || typeof error !== 'object') { | ||
| return false; | ||
| } | ||
|
|
||
| const errorObj = error as { | ||
| name?: string; | ||
| code?: string; | ||
| }; | ||
|
|
||
| return errorObj.name === 'AbortError' || errorObj.code === 'ERR_CANCELED'; | ||
| }; |
There was a problem hiding this comment.
The isAbortError function is duplicated from nodeInfo.ts. Consider importing it from nodeInfo.ts instead of duplicating the code to reduce code duplication and improve maintainability. You would need to export it from nodeInfo.ts first.
Summary
Verification