[storybook] PoC of Storybook directive, for inline stories#2910
[storybook] PoC of Storybook directive, for inline stories#2910clintandrewhall wants to merge 3 commits intomainfrom
Conversation
🔍 Preview links for changed docs |
📝 WalkthroughWalkthroughThis pull request adds support for a ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/Elastic.Documentation.Site/Assets/web-components/StorybookStory/StorybookStoryComponent.tsx`:
- Around line 50-70: attributeChangedCallback/mount can start overlapping mounts
when attributes change rapidly; add a guard to ensure only the latest mount
proceeds. Implement a per-instance version counter (e.g., this._mountVersion) or
an AbortController token that you increment/create at the start of mount(),
capture in local scope, and check/abort before/after awaiting loadBundle and
before calling this.bundle.mountStory; ensure any previous pending mount is
considered stale (skip rendering into this.container) if its version/token
doesn't match the current one. Update attributeChangedCallback to call mount()
as before but rely on the new guard inside mount() to prevent concurrent
renders.
In `@src/Elastic.Markdown/Myst/Directives/Storybook/StorybookBlock.cs`:
- Around line 136-140: In StorybookBlock (the method that validates the :root:
Storybook URI), trim trailing slashes from rootUri.AbsolutePath (or trim rawRoot
before creating/normalizing the Uri) before checking for "/iframe.html" so
values like ".../iframe.html/" are caught; update the same check/trim logic used
around the other iframe guard (the block referenced at lines 172-176) so both
places perform the trim/normalization prior to the EndsWith/Equals checks and
then proceed with the existing normalization and error assignment.
In `@src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs`:
- Around line 794-797: The attributes written for the <storybook> element are
not escaped: sanitize/HTML-encode the attribute values before writing them
(escape block.IframeTitle and block.StoryUrl output used in the src attribute,
and any other attribute values like block.Height if it's not strictly numeric),
e.g. call an existing utility or add a small helper (use
LlmRenderingHelpers.MakeAbsoluteUrl for URL resolution then pass that result
through an HTML-attribute-encoding method) and replace direct interpolations in
the renderer.Writer.Write calls so that quotes, ampersands and angle brackets
are encoded and cannot break or inject markup.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e3c4ca7c-c50c-4f00-9611-8945ddab6588
📒 Files selected for processing (26)
docs/_docset.ymldocs/syntax/directives.mddocs/syntax/index.mddocs/syntax/storybook.mdsrc/Elastic.Documentation.Configuration/Builder/ConfigurationFile.cssrc/Elastic.Documentation.Configuration/Toc/DocumentationSetFile.cssrc/Elastic.Documentation.Site/Assets/main.tssrc/Elastic.Documentation.Site/Assets/markdown/storybook.csssrc/Elastic.Documentation.Site/Assets/styles.csssrc/Elastic.Documentation.Site/Assets/web-components/StorybookStory/StorybookStoryComponent.tsxsrc/Elastic.Markdown/Myst/Directives/DirectiveBlockParser.cssrc/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cssrc/Elastic.Markdown/Myst/Directives/Storybook/StorybookBlock.cssrc/Elastic.Markdown/Myst/Directives/Storybook/StorybookView.cshtmlsrc/Elastic.Markdown/Myst/Directives/Storybook/StorybookViewModel.cssrc/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cssrc/Elastic.Markdown/Myst/Renderers/PlainText/PlainTextBlockRenderers.csstorybook-kibana-local-testing.mdstorybook-kibana-mdx-transform.mdtests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cstests/Elastic.Markdown.Tests/Directives/StorybookTests.cstests/Elastic.Markdown.Tests/MockFileSystemExtensions.cstests/authoring/Blocks/Storybook.fstests/authoring/Framework/Setup.fstests/authoring/LlmMarkdown/LlmMarkdownOutput.fstests/authoring/authoring.fsproj
| attributeChangedCallback() { | ||
| if (this.container) { | ||
| this.mount() | ||
| } | ||
| } | ||
|
|
||
| private async mount() { | ||
| const storyId = this.getAttribute('story-id') | ||
| const bundleUrl = this.getAttribute('bundle') | ||
|
|
||
| if (!storyId || !bundleUrl || !this.container) return | ||
|
|
||
| try { | ||
| this.bundle = await loadBundle(bundleUrl) | ||
| await this.bundle.mountStory(storyId, this.container) | ||
| } catch (err) { | ||
| if (this.container) { | ||
| this.container.innerHTML = `<div style="padding:1rem;color:#BD271E;font-size:14px">Failed to load story: ${err instanceof Error ? err.message : String(err)}</div>` | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential race condition on rapid attribute changes.
If attributes change while mount() is still awaiting the bundle load, a second mount() can start before the first completes, potentially rendering multiple stories into the same container. For a PoC this is acceptable, but consider adding a guard (abort controller or version counter) before production use.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/Elastic.Documentation.Site/Assets/web-components/StorybookStory/StorybookStoryComponent.tsx`
around lines 50 - 70, attributeChangedCallback/mount can start overlapping
mounts when attributes change rapidly; add a guard to ensure only the latest
mount proceeds. Implement a per-instance version counter (e.g.,
this._mountVersion) or an AbortController token that you increment/create at the
start of mount(), capture in local scope, and check/abort before/after awaiting
loadBundle and before calling this.bundle.mountStory; ensure any previous
pending mount is considered stale (skip rendering into this.container) if its
version/token doesn't match the current one. Update attributeChangedCallback to
call mount() as before but rely on the new guard inside mount() to prevent
concurrent renders.
| if (rootUri.AbsolutePath.EndsWith("/iframe.html", StringComparison.OrdinalIgnoreCase) | ||
| || rootUri.AbsolutePath.Equals("/iframe.html", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| validationError = $"storybook directive :root: should point to the Storybook root, not iframe.html. Got: {rawRoot}"; | ||
| return false; |
There was a problem hiding this comment.
Trim the path before the iframe.html guard.
Values ending in .../iframe.html/ slip past both checks because the trailing slash is removed after validation. They then normalize to .../iframe.html and produce .../iframe.html/iframe.html?..., which breaks the embed URL.
Proposed fix
- if (rootUri.AbsolutePath.EndsWith("/iframe.html", StringComparison.OrdinalIgnoreCase)
- || rootUri.AbsolutePath.Equals("/iframe.html", StringComparison.OrdinalIgnoreCase))
+ var rootPath = rootUri.AbsolutePath.TrimEnd('/');
+ if (rootPath.EndsWith("/iframe.html", StringComparison.OrdinalIgnoreCase))
{
validationError = $"storybook directive :root: should point to the Storybook root, not iframe.html. Got: {rawRoot}";
return false;
}
@@
- if (serverUri.AbsolutePath.EndsWith("/iframe.html", StringComparison.OrdinalIgnoreCase)
- || serverUri.AbsolutePath.Equals("/iframe.html", StringComparison.OrdinalIgnoreCase))
+ var serverPath = serverUri.AbsolutePath.TrimEnd('/');
+ if (serverPath.EndsWith("/iframe.html", StringComparison.OrdinalIgnoreCase))
{
validationError = $"docset.yml storybook.server_root should point to the Storybook server, not iframe.html. Got: {rawServerRoot}";
return false;
}Also applies to: 172-176
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/Elastic.Markdown/Myst/Directives/Storybook/StorybookBlock.cs` around
lines 136 - 140, In StorybookBlock (the method that validates the :root:
Storybook URI), trim trailing slashes from rootUri.AbsolutePath (or trim rawRoot
before creating/normalizing the Uri) before checking for "/iframe.html" so
values like ".../iframe.html/" are caught; update the same check/trim logic used
around the other iframe guard (the block referenced at lines 172-176) so both
places perform the trim/normalization prior to the EndsWith/Equals checks and
then proceed with the existing normalization and error assignment.
| renderer.Writer.Write("<storybook"); | ||
| renderer.Writer.Write($" src=\"{LlmRenderingHelpers.MakeAbsoluteUrl(renderer, block.StoryUrl)}\""); | ||
| renderer.Writer.Write($" height=\"{block.Height}\""); | ||
| renderer.Writer.Write($" title=\"{block.IframeTitle}\""); |
There was a problem hiding this comment.
Escape the serialized <storybook> attributes.
title is author-controlled and is written straight into a quoted attribute here. A value containing "/</& will break the structured tag or inject extra markup into the downstream LLM payload, and src should go through the same serialization path instead of raw interpolation. Since this element is the contract consumers parse, this needs proper attribute escaping before merge.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/Elastic.Markdown/Myst/Renderers/LlmMarkdown/LlmBlockRenderers.cs` around
lines 794 - 797, The attributes written for the <storybook> element are not
escaped: sanitize/HTML-encode the attribute values before writing them (escape
block.IframeTitle and block.StoryUrl output used in the src attribute, and any
other attribute values like block.Height if it's not strictly numeric), e.g.
call an existing utility or add a small helper (use
LlmRenderingHelpers.MakeAbsoluteUrl for URL resolution then pass that result
through an HTML-attribute-encoding method) and replace direct interpolations in
the renderer.Writer.Write calls so that quotes, ampersands and angle brackets
are encoded and cannot break or inject markup.
Summary
This PR is a proof-of-concept for embedding Kibana Storybook stories in
docs-builderoutput, so teams can render interactive component examples directly inside product documentation.The immediate problem we are trying to solve is that Storybook content today lives separately from our docs experience. We want a path where Kibana documentation can reference Storybook stories, have those stories render inside docs pages, and eventually support a workflow where Storybook-authored documentation can be previewed and published through the same docs system.
This implementation is intentionally early and exploratory. It was heavily AI-assisted to help move quickly on the shape of the integration, and I expect parts of the API and implementation to change based on review feedback, Kibana-side needs, and production deployment constraints.
Note
This PR was written with extensive assistance from GPT-5.4.
Approach
Rather than teaching
docs-builderto understand Storybook MDX directly, this POC adds a first-classstorybookdirective to the markdown engine and treats that as the stable contract.That means the current architecture is:
docs-builderconsumes that markdown via a{storybook}directive.This keeps
docs-builderfocused on rendering and validation, while leaving repo-specific Storybook MDX interpretation to Kibana if we choose to build that transform.What’s Included
New
storybookdirectiveAdds support for:
:::{storybook} :id: components-button--primary :title: Button / Primary story :::or, when needed:
:::{storybook} :root: /storybook/kibana-eui :id: components-button--primary :height: 320 :title: Button / Primary story :::The directive renders an iframe in HTML output and a structured
<storybook ...>element in LLM markdown output.Docset-level Storybook configuration
Adds
storybookconfig indocset.ymlso authors do not need to repeat the root on every story block.Supported settings now include:
This supports a few use cases:
storybook.rootstorybook.server_rootstorybook.allowed_rootsURL generation and validation
The directive now builds the iframe URL internally as:
Validation ensures that:
iframe.html, query strings, and fragments are rejected in:root:/is supported as the root of the Storybook serverDocumentation and testing notes
Adds and updates documentation for:
This is meant to make review easier and give the team a concrete starting point for trying the flow from Kibana.
Why this shape
The main design choice in this POC is that
docs-builderunderstands a normalized Storybook embed contract, not Storybook MDX itself.That gives us a cleaner separation of concerns:
docs-builderowns markdown parsing, validation, rendering, and docs-site outputThat feels like the right boundary for now, especially since Storybook MDX resolution is repo-specific and may vary by package, build system, or authoring conventions.
Notes / Caveats
/storybook/<library>/.Feedback I’m Looking For
docs-builder/ Kibana responsibility split the right one?storybook.root+server_rootthe right docset-level model?