Skip to content

Conversation

strickvl
Copy link
Contributor

@strickvl strickvl commented Sep 18, 2025

This PR enhances markdown rendering in the ZenML dashboard by adding support for GitHub Flavored Markdown (GFM) features, implementing security improvements, and adding a raw markdown view toggle.

Changes

✨ New Features

  • GFM Support: Added remark-gfm plugin to enable GitHub Flavored Markdown features:

    • Tables
    • Strikethrough text (~~text~~)
    • Task lists (- [ ] / - [x])
    • Autolinks
    • And more GFM extensions
  • Markdown View Toggle: Added ability to switch between preview and raw modes for markdown artifact visualizations:

    • Preview mode: Renders formatted markdown with GFM support
    • Raw mode: Shows the unparsed markdown source text with wrapping for readability
    • Per-visualization state management to remember mode selection during the artifact sheet session
    • Toggle only appears for confirmed markdown visualizations

🔒 Security Improvements

  • Centralized Markdown Component: Created a new secure Markdown component (src/components/Markdown.tsx) that:
    • Sanitizes all URLs (both href and src attributes) to prevent XSS attacks
    • Sets safe defaults for external links (target="_blank", rel="noopener noreferrer")
    • Filters out potentially unsafe content

🔧 Refactoring

  • Replaced direct react-markdown imports with the new centralized component in:
    • src/components/artifacts/MarkdownVisualization.tsx
    • src/app/overview/pipelines-grid/pipeline-sheet.tsx
  • Moved useArtifactVisualization hook before early returns to comply with React hooks rules

🐛 Bug Fixes

  • Fixed unnecessary escape character in password validation regex

Technical Details

New Components & Context

  1. MarkdownViewToggle (src/components/artifacts/MarkdownViewToggle.tsx):

    • Button group for switching between Preview and Raw modes
    • Uses design system button props for accessible state indication
    • Includes aria-pressed for screen reader support
  2. VisualizationUIStateContext (src/context/VisualizationUIStateContext.tsx):

    • Manages per-visualization UI preferences (currently markdown render mode)
    • Scoped to artifact sheet lifetime to avoid leaking preferences across sheets
    • Uses index-based state tracking since visualization list is stable during a session
    • Optimized with useCallback and useMemo to prevent unnecessary re-renders
    • Only persists non-default states to keep memory footprint small
  3. Secure Markdown Component (src/components/Markdown.tsx):

    • Extends react-markdown with the remark-gfm plugin
    • Provides default secure components for <a> and <img> tags
    • Uses URL sanitization via sanitizeUrl utility
    • Allows component overrides while maintaining security defaults

Dependencies Added

  • remark-gfm@^4.0.0 - Official plugin for GFM support in react-markdown

Testing

  • ✅ Verify markdown rendering in artifact visualizations
  • ✅ Confirm GFM features work (tables, task lists, etc.)
  • ✅ Check that URLs are properly sanitized
  • ✅ Ensure external links open in new tabs with proper security attributes
  • ✅ Test markdown view toggle switches between preview and raw modes
  • ✅ Verify toggle state persists within the same artifact sheet session
  • ✅ Confirm toggle only appears for markdown visualizations after confirmation

@strickvl strickvl added bug Something isn't working vibecoded labels Sep 18, 2025
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR enables GitHub Flavored Markdown (GFM) rendering for dashboard components by creating a centralized Markdown component with URL sanitization and security features.

  • Replaces direct react-markdown usage with a custom Markdown wrapper component
  • Adds remark-gfm plugin support for enhanced markdown features like tables and strikethrough
  • Implements URL sanitization for links and images to prevent security vulnerabilities

Reviewed Changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated no comments.

File Description
src/components/Markdown.tsx New centralized Markdown component with GFM support and security features
src/components/artifacts/MarkdownVisualization.tsx Updated to use the new custom Markdown component
src/app/overview/pipelines-grid/pipeline-sheet.tsx Updated to use the new custom Markdown component
package.json Added remark-gfm dependency for GFM rendering support
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link
Contributor

coderabbitai bot commented Sep 18, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Added remark-gfm to dependencies. Introduced a new Markdown component that wraps react-markdown with GFM and URL sanitization for links and images. Updated two consumers to use the new local Markdown component instead of react-markdown. No other logic or public API changes.

Changes

Cohort / File(s) Summary
Dependencies
package.json
Added dependency remark-gfm@^4.0.0.
New Markdown component
src/components/Markdown.tsx
Added Markdown wrapper around react-markdown with GFM enabled; sanitizes href on links and src on images; merges custom component overrides; default export.
Import migration to local Markdown
src/app/overview/pipelines-grid/pipeline-sheet.tsx, src/components/artifacts/MarkdownVisualization.tsx
Switched imports from react-markdown to local @/components/Markdown; usage unchanged.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor UI as Caller (PipelineSheet / MarkdownVisualization)
  participant MD as Markdown (local wrapper)
  participant RM as ReactMarkdown
  participant GFM as remark-gfm
  participant SAN as URL Sanitizer

  UI->>MD: render(children, components?, className?)
  Note over MD: Merge default link/image overrides with user components
  MD->>RM: <ReactMarkdown remarkPlugins=[GFM] ...>
  RM->>GFM: Parse Markdown (GFM extensions)
  par Link rendering
    RM->>SAN: sanitizeUrl(href)
    alt safe or empty
      SAN-->>RM: sanitized href
      RM-->>MD: <a href target="_blank" rel="noopener noreferrer">
    else unsafe
      SAN-->>RM: unsafe/null
      RM-->>MD: <a> (no href)
    end
  and Image rendering
    RM->>SAN: sanitizeUrl(src)
    alt safe
      SAN-->>RM: sanitized src
      RM-->>MD: <img src ...>
    else unsafe
      SAN-->>RM: null
      RM-->>MD: skip image
    end
  end
  MD-->>UI: Rendered HTML
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

I nibble notes in Markdown meadows bright,
With GFM sprigs and links made safe and light.
I hop past traps where shady URLs hide,
Images vetted, I bound with pride.
New wrapper burrows snug and sound—
Now every paragraph’s a carrot crown! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly and concisely describes the primary change of enabling GitHub Flavored Markdown rendering across the dashboard, which aligns with the added remark-gfm dependency, new Markdown wrapper component, and import updates; it avoids unnecessary detail while conveying the main enhancement.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@strickvl strickvl changed the base branch from main to staging September 18, 2025 14:25
@strickvl strickvl changed the title Enable gfm rendering for dashboard Enable GitHub Flavored Markdown (GFM) rendering for dashboard Sep 18, 2025
@strickvl strickvl linked an issue Sep 18, 2025 that may be closed by this pull request
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/components/Markdown.tsx (2)

17-27: Avoid rendering <a> without href; fallback to non-interactive element.

When a URL is unsafe/absent, render a span to prevent confusing keyboard/focus semantics.

-      return (
-        <a {...props} href={safeHref} target={anchorTarget} rel={anchorRel}>
-          {children}
-        </a>
-      );
+      if (!safeHref) {
+        return <span {...props}>{children}</span>;
+      }
+      return (
+        <a {...props} href={safeHref} target={anchorTarget} rel={anchorRel}>
+          {children}
+        </a>
+      );

28-32: Add image loading/perf safeguards.

Lazy-load images and decode asynchronously; also drop referrer info.

-      return <img {...props} src={sanitized} alt={alt ?? ""} />;
+      return (
+        <img
+          {...props}
+          src={sanitized}
+          alt={alt ?? ""}
+          loading="lazy"
+          decoding="async"
+          referrerPolicy="no-referrer"
+        />
+      );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b28f83 and fbebb81.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • package.json (1 hunks)
  • src/app/overview/pipelines-grid/pipeline-sheet.tsx (1 hunks)
  • src/components/Markdown.tsx (1 hunks)
  • src/components/artifacts/MarkdownVisualization.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/Markdown.tsx (1)
src/lib/url.ts (1)
  • sanitizeUrl (64-66)
🔇 Additional comments (3)
src/app/overview/pipelines-grid/pipeline-sheet.tsx (1)

23-23: Swapping to local Markdown wrapper is correct.

This centralizes GFM + sanitization for the sheet summary. No further changes needed here.

src/components/artifacts/MarkdownVisualization.tsx (1)

2-2: Good switch to the wrapped Markdown.

Keeps visualization consistent with GFM and sanitization.

package.json (1)

53-53: Add remark-gfm — lockfile updated; no stray react-markdown imports.

pnpm-lock.yaml contains [email protected] and react-markdown is only imported in src/components/Markdown.tsx (the wrapper) which already uses remarkGfm; refresh CI dependency caches.

}
};

const mergedComponents: Components = { ...defaultComponents, ...components };
Copy link
Contributor

@coderabbitai coderabbitai bot Sep 18, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Do not allow overriding sanitized a and img components.

Merging order currently lets callers replace the secure defaults, weakening guarantees. Lock these two to preserve sanitization.

Apply:

- const mergedComponents: Components = { ...defaultComponents, ...components };
+ const mergedComponents: Components = {
+   ...components,
+   a: defaultComponents.a,
+   img: defaultComponents.img
+ };
📝 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.

Suggested change
const mergedComponents: Components = { ...defaultComponents, ...components };
const mergedComponents: Components = {
...defaultComponents,
...components,
a: defaultComponents.a,
img: defaultComponents.img
};
🤖 Prompt for AI Agents
In src/components/Markdown.tsx around line 35, the current merge lets callers
override sanitized anchor and image renderers; ensure the sanitized defaults
cannot be replaced by always enforcing defaultComponents.a and
defaultComponents.img after merging user components. Implement this by merging
user components into defaults as now, then explicitly set mergedComponents.a =
defaultComponents.a and mergedComponents.img = defaultComponents.img (or remove
any incoming a/img keys before merge) so callers cannot override these two keys
while keeping other custom components honored.

Copy link
Contributor

Choose a reason for hiding this comment

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

As of right now, I dont see a need for users to pass in their custom components. For now I think its fine to use the predefined set fo components, which means we can adjust the props type, and the merge behavior (as its not required). This should solve this issue.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Cahllagerfeld You're absolutely right! Removing the custom components functionality is a much cleaner solution that eliminates the security concern entirely. Here's the updated implementation:

import ReactMarkdown from "react-markdown";
import type { Options } from "react-markdown";
import remarkGfm from "remark-gfm";
import { sanitizeUrl } from "@/lib/url";

type MarkdownProps = Omit<
	Options,
	"children" | "remarkPlugins" | "rehypePlugins" | "components"
> & {
	children?: string | null;
	className?: string;
};

export default function Markdown({ children, className, ...rest }: MarkdownProps) {
	const components = {
		a({ href, node: _node, children, target, rel, ...props }) {
			const sanitized = href ? sanitizeUrl(href) : undefined;
			const safeHref = sanitized && sanitized.startsWith("unsafe:") ? undefined : sanitized;
			const anchorTarget = target ?? "_blank";
			const anchorRel = rel ?? "noopener noreferrer";
			return (
				<a {...props} href={safeHref} target={anchorTarget} rel={anchorRel}>
					{children}
				</a>
			);
		},
		img({ src, alt, node: _node, ...props }) {
			const sanitized = src ? sanitizeUrl(src) : undefined;
			if (!sanitized || sanitized.startsWith("unsafe:")) return null;
			return <img {...props} src={sanitized} alt={alt ?? ""} />;
		}
	};

	return (
		<ReactMarkdown
			{...rest}
			className={className}
			components={components}
			remarkPlugins={[remarkGfm]}
		>
			{children ?? ""}
		</ReactMarkdown>
	);
}

This approach:

  • Removes the components prop from the type and function signature
  • Eliminates the merge logic that created the security vulnerability
  • Keeps the sanitized a and img components locked in place
  • Simplifies the overall implementation

Perfect solution that addresses both security and simplicity! 🎯

@bcdurak bcdurak requested a review from tzador September 24, 2025 14:50
Copy link
Contributor

@tzador tzador left a comment

Choose a reason for hiding this comment

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

Nice. Would be great to have a small button which would show the original markdown source

strickvl and others added 2 commits October 3, 2025 10:21
- Add MarkdownViewToggle component for switching between preview and raw modes
- Create VisualizationUIStateContext to manage per-visualization UI preferences
- Move useArtifactVisualization hook before early returns to fix React hooks rule
- Fix regex escape in password validation schema
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[0-9]/, "Password must contain at least one number")
.regex(/[!@#$%^&*(),.?":{}|<>\/]/, "Password must contain at least one special character");
.regex(/[!@#$%^&*(),.?":{}|<>/]/, "Password must contain at least one special character");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This change the linter suggested. Feels like I shouldn't touch it, but idk...

Copy link
Contributor

Choose a reason for hiding this comment

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

yep, forward slash doesn't need escaping

- Add .claude and design/ to .gitignore to prevent tracking of these files in version control.
@strickvl strickvl requested a review from tzador October 3, 2025 09:08
Copy link
Contributor

@tzador tzador left a comment

Choose a reason for hiding this comment

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

LG

.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[0-9]/, "Password must contain at least one number")
.regex(/[!@#$%^&*(),.?":{}|<>\/]/, "Password must contain at least one special character");
.regex(/[!@#$%^&*(),.?":{}|<>/]/, "Password must contain at least one special character");
Copy link
Contributor

Choose a reason for hiding this comment

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

yep, forward slash doesn't need escaping

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working vibecoded

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MarkdownString tables aren't rendered

3 participants