Skip to content

feat(docs): render gRPC requests as read-only documentation pages - #36

Merged
arpit-bruno merged 18 commits into
usebruno:mainfrom
sundram-bruno:feat/grpc-docs-page
Aug 13, 2026
Merged

feat(docs): render gRPC requests as read-only documentation pages#36
arpit-bruno merged 18 commits into
usebruno:mainfrom
sundram-bruno:feat/grpc-docs-page

Conversation

@sundram-bruno

@sundram-bruno sundram-bruno commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

Ref: BRU-3719

Introduces a read-only documentation page for gRPC requests. Until now a gRPC request in a collection showed "preview not available".

The page shows the proto file name (not shown in case of reflection), the RPC method with its call type, the request messages, metadata and auth, execution context and generates a grpcURL command alongside. A JavaScript snippet is offered too, but only when the request has a proto file attached, since the Node client is built from a file on disk and cannot be built from server reflection.

The playground still cannot execute gRPC, so the unsupported-request check is split into a docs one and a playground one.

2026-08-07_19-04-07 image

@sundram-bruno
sundram-bruno changed the base branch from release/v0.2.0 to main August 7, 2026 13:38
@sundram-bruno
sundram-bruno marked this pull request as draft August 7, 2026 14:30
gRPC requests previously fell through to the "preview not available"
state. They now get a docs page showing the proto file name, the RPC
method and its call type, request messages, metadata and auth, with
generated grpcURL and JavaScript snippets alongside.

The JavaScript snippet is only offered when a proto file is attached,
since @grpc/proto-loader builds the client from a file on disk and
cannot read a service definition from server reflection.

The playground still cannot execute gRPC, so the unsupported-request
predicate is split into separate docs and playground checks.

Ref: BRU-3719
…ppet

The proto folder, proto filename, server address and method name were
interpolated into the command unquoted, so a value carrying a space split
into several shell words and one carrying shell syntax executed when the
reader pasted the command. Headers and the message body were already
quoted; these three were missed.

Verified against grpcurl 1.9.3: the unquoted form of a spaced proto path
fails with "Too many arguments", the quoted form completes the call.
The plaintext flag was read from the url as written. A collection that keeps
its address in a variable hides the scheme there, so the flag fell back to
assuming no encryption and every such request was sent -plaintext. Against a
TLS server grpcurl then waits for the timeout and reports a context deadline,
naming neither TLS nor the flag, so the failure reads as an unreachable server.

The flag now comes from the variable's value, read through lookup rather than
resolve so the show-variables toggle cannot change it. A secret value is never
read, and the address itself is still emitted as written, so the token stays
hoverable.

The Prod environment pointed grpcUrl at port 443 over grpc://, which is the
TLS port; it now reads grpcs://.
A heredoc ends at the first line equal to its delimiter, so a streaming
message containing a bare EOF line closed it early and handed the rest of
the message to the shell as commands. The delimiter is now chosen to avoid
every line the messages contain.

The service path and method name are written into JavaScript positions no
string escape can protect — `new proto.<path>()` and `client.<name>()` — so
a method that is not a plain identifier path produced code that both ran
arbitrary statements and failed to parse. Both are validated, and anything
else yields no snippet, matching the existing no-service behaviour.

parseProtoFlags dropped the leading empty segment when splitting, so an
absolute /protos/book.proto silently pointed grpcurl at a relative protos.

Also folds three copies of the leading-slash strip into one exported
helper, renames isClientStreaming to streamsInFor since it covered bidi
too, and removes a branch that re-tested the condition it had just used.
A gRPC request carries variables, scripts, assertions and post-response
captures in its runtime block, and the converter writes all four, but the
docs page dropped the lot. An HTTP request in the same collection showed
them, so a scripted gRPC request silently documented less than its
neighbour.

The collectors already read the generic runtime paths and were only typed
to HttpRequest, so they are widened to Item rather than duplicated. Post-
response captures live in runtime.actions for every protocol; the published
GrpcRequestRuntime omits that field, so it is read structurally until the
types catch up.

The sample Order Service gains a runtime block so the section renders real
content in the docs and the tests assert against it.
The message card re-declared the chevron's transition and rotation, which
ChevronArrow already owns. Because the copy sat outside a media query it
could override ChevronArrow's prefers-reduced-motion opt-out and animate for
a reader who asked for no motion. The card's own open/close transition had
no such guard at all, unlike the example card it was modelled on.

The snippets memo listed metadata and messages, which the getters rebuild on
every render, so both generators re-ran every time; the getters are memoized
on the item instead. An omitted ancestry prop defaulted to a fresh array,
invalidating four more memos for the same reason.

Also documents why the message body mounts during render and why a collapsed
body is marked inert, neither of which is apparent from the code, and adds
the default export every sibling component has.
Adding an isHttpRequest guard to route gRPC away from the HTTP page turned
the previously unconditional fall-through into a bare null, so an item with
an unrecognised or missing type rendered a completely blank page — no
breadcrumb, no title, nothing to distinguish it from a broken app. Such an
item is reachable: navModel routes anything that is not a folder or a script
to the request page.

Restores the fall-through to RequestContent, which is what shipped before,
and covers it so the next type guard cannot bring the blank page back.
…t ids

The method badge inferred "render as written" from the casing of the value,
so any collection storing a mixed-case HTTP method rendered it unchanged —
Patch stayed Patch instead of PATCH. The escape hatch exists for gRPC alone,
so it is now an explicit prop the gRPC url bar opts into.

A message body was pasted straight into a code position in the JavaScript
snippet. It is now parsed first: a body that is not JSON becomes a quoted
string rather than executable text, while a templated body stays verbatim so
its variables survive.

Snippet child test ids derive from the testId prop instead of a hardcoded
prefix, so the same component can be located under the request, example and
gRPC bases; CodeSnippetComponent takes that base as a parameter.

Also drops a spec helper that became identical to its neighbour once
getRequestAuth was widened, re-homes a test that sat outside its describe,
removes an unused e2e locator, and restates the method-colour comment as a
timeless fact.
…Tabs

hideHeader removed the thead outright, taking the th scope="col" cells with
it, so a screen reader lost the column each metadata cell belonged to. The
header now stays in the markup and is hidden visually, reusing the same clip
technique the table caption already uses.

SnippetTabs had no spec of its own and was only exercised through the HTTP
wrapper, so the empty, embedded and test-id-derivation branches were unread.
…rry auth

A methodType read straight from a collection indexed the icon map without a
guard, so a request typed "toString" resolved Object.prototype.toString, passed
the truthiness check and rendered undefined as a component — the whole request
page threw. The lookup is now an own-property check, the same class of hole as
the unrecognised-request-type fall-through fixed earlier on this branch.

A message body containing a variable skipped the JSON check and was written
into the JavaScript snippet verbatim, so a body could append statements that
run when the reader pastes it. The body is now probed with its variables
replaced by a JSON-safe value, which keeps a genuine templated body intact and
quotes anything that is not a lone JSON document.

The page showed an Auth section while both snippets dialled with no
credentials. They now carry it through the converter the HTTP snippets already
use, so the two protocols express the same auth the same way, including the
note for auth that cannot become a header.

Also groups the config sections under Configuration in the section nav as the
HTTP page does, renames a shadowed local and parseProtoFlags, and composes the
parameterised CodeSnippetComponent in the gRPC page object.
The label lookup indexed a plain object with a value read from the collection,
the same unguarded shape as the icon map fixed alongside it, so a methodType
named after an Object member resolved a function and rendered it as a child.

The show-more control in the messages list was the view-more control with
different words: same button, same chevron, same rotation and reduced-motion
rules. Both now render one ExpandToggle, so the two cannot drift and the
reduced-motion opt-out lives in a single place.

The message card also mounts its body from the toggle handler rather than
during render, which is how the example card it was modelled on does it.

On the test side, the messages locators become a component that derives its
child ids from a base, the gRPC page object annotates its locators, and the
examples component composes the parameterised snippet component instead of
naming its ids by hand.
@sundram-bruno
sundram-bruno marked this pull request as ready for review August 9, 2026 20:03

@vasharma05-bruno vasharma05-bruno left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some review comments over all PRs:

  1. Claude tests are mixed b/w renderToStaticMarkup and useRenderToDom. Please use useRenderToDom.
  2. The svg width and height is not matching the viewbox. Please use the width = height = viewbox's width and height.
  3. Please use path alias for all your changed files.
  4. Please use functions from test-utils/dom.ts
  5. Please use cx function
  6. Please revert the types changed from HttpRequest to Item. Item can be a folder/ collection/ request, etc.
  7. GrpcMessageCard component changes as discussed.

Comment thread packages/bruno-api-docs/src/assets/icons/BidiStreamingIcon.tsx Outdated
Comment thread packages/bruno-api-docs/src/assets/icons/BidiStreamingIcon.tsx Outdated
Comment thread packages/bruno-api-docs/src/assets/icons/ClientStreamingIcon.tsx Outdated
Comment thread packages/bruno-api-docs/src/assets/icons/ServerStreamingIcon.tsx Outdated
Comment thread packages/bruno-api-docs/src/assets/icons/UnaryIcon.tsx Outdated
Comment thread packages/bruno-api-docs/src/components/Request/RequestUrlBar/RequestUrlBar.tsx Outdated
Comment thread packages/bruno-api-docs/src/components/ViewMore/ViewMore.tsx Outdated
Comment thread packages/bruno-api-docs/src/pages/Request/Request.tsx Outdated
Comment thread packages/bruno-api-docs/src/utils/assertions.ts Outdated
Comment thread packages/bruno-api-docs/src/utils/fileUtils.ts Outdated
Narrows the request helpers to HttpRequest | GrpcRequest rather than Item, so
the signature carries what the caller actually holds and the casts each call
site needed disappear. resolveInheritedAuth and getInheritedAuthSummary stay
Item: the folder settings view passes a folder to both, so narrowing them
would break it.

The four streaming icons take their stroke styling from baseIconProps, and
the expand toggle reuses ChevronDownIcon instead of inlining a copy of it,
which also lets the icon size come from a prop rather than a css override.

The specs this PR added render through useRenderToDom, imports in the files
it created use the path alias, class names go through cx, and the two
margin-only rules become utility classes.

MethodBadge reads capitalizeMethod, defaulting to true, so the gRPC call site
says what it wants rather than naming the outcome.
@sundram-bruno

Copy link
Copy Markdown
Contributor Author

@vasharma05-bruno addressed the comments, can you please review.

The expand toggle spec finds its button by test id rather than by a data
attribute selector, and imports through the path alias.

The field text declared font-weight 400, which is the value it would have
inherited anyway.

@vasharma05-bruno vasharma05-bruno left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things that got left out.

Also, most of the tests are still using markup (html content search), rather than properly searching for elements in a DOM tree using useRenderToDom. The tests are now using useRenderToDom, but is extracting the innerHtml from it.

Comment thread packages/bruno-api-docs/src/components/GrpcRequestContent/GrpcRequestContent.tsx Outdated
Comment thread packages/bruno-api-docs/src/components/SnippetTabs/SnippetTabs.tsx Outdated

@vasharma05-bruno vasharma05-bruno left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@arpit-bruno
arpit-bruno merged commit 5f63f21 into usebruno:main Aug 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants