Repository navigation
refactor(servers): use shared Field in TestConnectionPanel - #180
CloudCorpRecords wants to merge 2 commits into
Conversation
marekdano
left a comment
There was a problem hiding this comment.
@CloudCorpRecords - thanks for your contribution!!!
Findings
| Severity | Location | Problem | Fix |
|---|---|---|---|
| blocking | CI: DCO check | Commit cb53f710 has no Signed-off-by trailer. |
Add it: git commit -s --amend (or a follow-up commit with -s), then force-push. |
| minor | TestConnectionPanel.tsx:490 |
Headers field's hint paragraph now renders before the error paragraph; the visible error reads second instead of first. | Optional: reorder so the error paragraph still renders first. |
| minor | TestConnectionPanel.tsx:494 |
Headers field manually splices aria-describedby because Field suppresses hint whenever error is set. |
Optional: extend Field to support hint + error together natively. |
Merge is blocked only on the DCO check. The two UI notes are optional polish.
cb53f71 to
0664ef0
Compare
|
Thanks for the review, @marekdano. DCO sign-off added (amended and force-pushed), so that check is green now. On the two optional notes:
Let me know which way you'd like to go. |
marekdano
left a comment
There was a problem hiding this comment.
@CloudCorpRecords - Thanks for your fixes!
New findings
1. Hint/error DOM order (functionally-impacting)
Root cause: Field's hint prop hides whenever an error is present (it's meant as a redundant explainer, not needed once the error message appears). The Headers field needs an always-visible informational note (stored-credentials hint) that coexists with a validation error, so the component bypassed hint and hand-rolled the paragraph inside the children render-prop — which runs before Field's own error paragraph. That's the swap.
Fix — add a new prop to the shared field.tsx instead of patching around it locally:
// field.tsx
interface FieldProps {
// ...
/** Always-visible info text, shown even while `error` is set — unlike `hint`,
* which hides once an error explains the problem. */
persistentHint?: React.ReactNode;
}
function Field({ /* ... */ persistentHint, /* ... */ }: FieldProps) {
const persistentHintId = persistentHint ? `${id}-persistent-hint` : undefined;
const controlProps: FieldControlProps = {
id,
"aria-invalid": hasError,
"aria-describedby":
[errorId, hintId, persistentHintId].filter(Boolean).join(" ") || undefined,
// ...
};
return (
// ...
{children(controlProps)}
{hintId && <p id={hintId} ...>{hint}</p>}
{hasError && <p id={errorId} role="alert" ...>{error}</p>}
{persistentHintId && (
<p id={persistentHintId} className="text-[13px] text-muted-foreground">
{persistentHint}
</p>
)}
// ...
);
}// TestConnectionPanel.tsx — Headers field
<Field
id="headers"
label="Headers"
info="Request headers as a JSON object."
error={errors.headers}
persistentHint={
mode === "handshake"
? intl.formatMessage({ id: "mcpServer.testConnection.storedCredentialsHint" })
: undefined
}
>
{(controlProps) => (
<Textarea
{...controlProps}
value={headers}
onChange={(e) => {
setHeaders(e.target.value);
clearError("headers");
}}
onBlur={() => setErrors((prev) => ({ ...prev, headers: validateHeaders(headers) }))}
placeholder="Add request headers as JSON..."
className="bg-transparent font-mono text-sm focus-visible:ring-1 focus-visible:ring-offset-0"
disabled={isTesting}
/>
)}
</Field>Drop the manual aria-describedby merge and the hand-rolled <p id="headers-hint"> — Field now owns both.
2. Undocumented workaround (suggestion)
Solved as a side effect: persistentHint carries its own doc comment explaining why it's separate from hint, so there's no silent bypass left to misread or "simplify" away.
3. Explicit aria-invalid="false" on neutral fields (minor)
// field.tsx
const controlProps: FieldControlProps = {
id,
"aria-invalid": hasError || undefined, // was: hasError
"aria-describedby": [errorId, hintId, persistentHintId].filter(Boolean).join(" ") || undefined,
"aria-required": required !== undefined ? required : undefined,
};Omits the attribute entirely when there's no error (matches pre-refactor DOM), still renders aria-invalid="true" on real errors. One line, benefits every Field-based field, not just this one.
| <FieldLabel htmlFor="test-mode"> | ||
| {intl.formatMessage({ id: "mcpServer.testConnection.mode.label" })} | ||
| </FieldLabel> | ||
| <Select value={mode} onValueChange={handleModeChange} disabled={isTesting}> |
There was a problem hiding this comment.
Keep the details drawer open when dismissing field help. These new info props open Radix popovers, but MCPServerDetailsPanel closes on every Escape, including events the popover already consumed. Repro: open a server's Try it panel, open URL help, and press Escape; the drawer closes too. Please make the drawer respect consumed Escape events and add a container-level regression test so dismissing help preserves the connection form.
Replace the local FieldLabel and hand-rolled error markup with the shared Field component, so required/aria-invalid/aria-describedby wiring and the info popover come from one place. The Method radiogroup keeps a plain Label (out of scope per #7007). The handshake credentials hint still describes the Headers field alongside its error. Refs IBM/mcp-context-forge#7007 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Rene Turcios <95189389+CloudCorpRecords@users.noreply.github.com>
…umed Escape Address review on contextforge-org#180: - Field gains persistentHint: an always-visible note rendered after the error, wired into aria-describedby alongside error/hint. Headers now uses it instead of hand-rolling the stored-credentials paragraph and merging aria-describedby locally, so the error reads before the note. - Field omits aria-invalid on a neutral control instead of rendering aria-invalid="false". - MCPServerDetailsPanel ignores an Escape a nested Radix layer already consumed (defaultPrevented), so dismissing a field-help popover no longer closes the drawer. Container-level regression test included. Refs IBM/mcp-context-forge#7007 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Signed-off-by: Rene Turcios <95189389+CloudCorpRecords@users.noreply.github.com>
|
Thanks @marekdano and @vishu-bh. Pushed 6482cdc (rebased on current
Lint is clean and the component and page suites pass locally (2,749 tests); full CI should confirm. |
0664ef0 to
6482cdc
Compare
Summary
First checklist item of IBM/mcp-context-forge#7007: replace the local
FieldLabelinTestConnectionPanel.tsxand its hand-rolled label/error markup with the sharedFieldcomponent.FieldLabeland converts its call sites:test-mode,url,path,content-type,headers,bodyrequired/aria-invalid/aria-describedby/ the error<p role="alert">now come fromFieldinstead of being re-implemented per input<title>tooltip hints becomeField'sinfopopover, which is keyboard-reachableMethodradiogroup keeps a plainLabel(out of scope in #7007: it's a group legend, not a single control)Behavior notes
aria-describedby, so Headers is still described by both its error and the hint (an existing test asserts this).Field's ownhintprop would hide the hint while an error is showing.Field's info trigger has the accessible name "More info about {label}", so loosegetByLabelText(/headers/i)//body/iqueries inTestConnectionPanel.test.tsxbecame ambiguous. I anchored them (/^headers/i,/^body/i), matching the existing/^url/iand/^path/i.Not touched:
CatalogOAuthDialog(open PR #177 rewrites it), and the other checklist files. Happy to do those in follow-ups.Test plan
vitest run src/components/servers/TestConnectionPanel.test.tsx(47/47)vitest runon servers, pages/Servers, pages/Teams, mcp-servers, server-catalog (311/311 in isolation)eslintandprettier --checkclean; pre-commit secret scan passesvitest runshowed load-related timeouts in unrelated files on my machine; those files pass individually, with and without this change.Refs IBM/mcp-context-forge#7007
🤖 Generated with Claude Code