-
Notifications
You must be signed in to change notification settings - Fork 93
Make browser context network-aware and render Mermaid diagrams #117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { Copy, CornerDownLeft, Ellipsis, ExternalLink, Globe, GlobeLock, Home, Loader2, Minus, Play, Plus, RefreshCw, SquareArrowOutUpRight, Trash2, Zap } from "lucide-react" | ||
| import { memo, useCallback, useEffect, useRef, useState, type FocusEvent, type FormEvent, type ReactNode } from "react" | ||
| import type { LocalHttpServerInfo, ProjectQuickAction } from "../../../shared/protocol" | ||
| import { browserOriginFromWindow, resolveUrlForBrowserHost } from "../../../shared/browser-context" | ||
| import type { KannaSocket } from "../../app/socket" | ||
| import { | ||
| getCachedLocalHttpServers, | ||
|
|
@@ -139,7 +140,7 @@ function BrowserPanelImpl({ projectId, socket, onRunQuickAction }: BrowserPanelP | |
|
|
||
| const openServer = useCallback(async (server: LocalHttpServerInfo) => { | ||
| if (!isCloud) { | ||
| navigateBrowser(projectId, server.address) | ||
| navigateBrowser(projectId, resolveUrlForBrowserHost(server.address, browserOriginFromWindow())) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Kanna is opened through the machine's LAN hostname or IP, this rewrites every discovered loopback URL to that hostname. Discovery does not retain whether the server listens only on loopback, so a server bound to |
||
| return | ||
| } | ||
| const publicUrl = await exposeServer(server) | ||
|
|
@@ -450,7 +451,8 @@ function BrowserPanelImpl({ projectId, socket, onRunQuickAction }: BrowserPanelP | |
| <div className="space-y-1.5"> | ||
| {visibleServers.map((server) => { | ||
| const isExposing = exposingPorts.has(server.port) | ||
| const openUrl = isCloud && server.publicUrl ? server.publicUrl : server.address | ||
| const reachableAddress = resolveUrlForBrowserHost(server.address, browserOriginFromWindow()) | ||
| const openUrl = isCloud && server.publicUrl ? server.publicUrl : reachableAddress | ||
| return ( | ||
| <ContextMenu key={server.address}> | ||
| <ContextMenuTrigger asChild> | ||
|
|
@@ -490,7 +492,7 @@ function BrowserPanelImpl({ projectId, socket, onRunQuickAction }: BrowserPanelP | |
| </span> | ||
| <span className="flex w-full min-w-0 items-center gap-3"> | ||
| <span className="min-w-0 flex-1 truncate text-xs text-muted-foreground"> | ||
| {isExposing ? "Exposing…" : server.publicUrl ?? server.address} | ||
| {isExposing ? "Exposing…" : server.publicUrl ?? reachableAddress} | ||
| </span> | ||
| {server.ownerPath ? ( | ||
| <span className="max-w-[45%] shrink-0 truncate text-right text-[11px] text-muted-foreground/70">{formatPathWithTilde(server.ownerPath)}</span> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| import { useEffect, useId, useState } from "react" | ||
| import { CopyButton } from "../ui/copy-button" | ||
|
|
||
| type MermaidTheme = "default" | "dark" | ||
|
|
||
| type DiagramState = | ||
| | { status: "loading" } | ||
| | { status: "ready"; svg: string } | ||
| | { status: "error" } | ||
|
|
||
| let renderSequence = 0 | ||
| let renderQueue = Promise.resolve() | ||
|
|
||
| function currentTheme(): MermaidTheme { | ||
| if (typeof document === "undefined") return "default" | ||
| return document.documentElement.classList.contains("dark") ? "dark" : "default" | ||
| } | ||
|
|
||
| function queueRender(source: string, id: string, theme: MermaidTheme) { | ||
| const render = renderQueue.then(async () => { | ||
| const { default: mermaid } = await import("mermaid") | ||
| mermaid.initialize({ | ||
| startOnLoad: false, | ||
| securityLevel: "strict", | ||
| suppressErrorRendering: true, | ||
| theme, | ||
| flowchart: { useMaxWidth: true }, | ||
| }) | ||
| return mermaid.render(id, source) | ||
| }) | ||
|
|
||
| renderQueue = render.then(() => undefined, () => undefined) | ||
| return render | ||
| } | ||
|
|
||
| export function MermaidDiagram({ source }: { source: string }) { | ||
| const reactId = useId().replace(/[^a-zA-Z0-9_-]/g, "") | ||
| const [theme, setTheme] = useState<MermaidTheme>(currentTheme) | ||
| const [state, setState] = useState<DiagramState>({ status: "loading" }) | ||
|
|
||
| useEffect(() => { | ||
| const root = document.documentElement | ||
| const observer = new MutationObserver(() => setTheme(currentTheme())) | ||
| observer.observe(root, { attributes: true, attributeFilter: ["class"] }) | ||
| return () => observer.disconnect() | ||
| }, []) | ||
|
|
||
| useEffect(() => { | ||
| let cancelled = false | ||
| setState({ status: "loading" }) | ||
| renderSequence += 1 | ||
|
|
||
| void queueRender(source, `kanna-mermaid-${reactId}-${renderSequence}`, theme) | ||
| .then(({ svg }) => { | ||
| if (!cancelled) setState({ status: "ready", svg }) | ||
| }) | ||
| .catch(() => { | ||
| if (!cancelled) setState({ status: "error" }) | ||
| }) | ||
|
|
||
| return () => { | ||
| cancelled = true | ||
| } | ||
| }, [reactId, source, theme]) | ||
|
|
||
| if (state.status === "ready") { | ||
| return ( | ||
| <div | ||
| data-mermaid-diagram | ||
| role="img" | ||
| aria-label="Mermaid diagram" | ||
| className="my-3 max-w-full overflow-x-auto rounded-xl border border-border bg-background p-4 [&_svg]:mx-auto [&_svg]:h-auto [&_svg]:max-w-full" | ||
| dangerouslySetInnerHTML={{ __html: state.svg }} | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| if (state.status === "error") { | ||
| return ( | ||
| <div data-mermaid-diagram className="group/mermaid relative my-3 max-w-full overflow-x-auto rounded-xl border border-border bg-background"> | ||
| <div className="border-b border-border px-3.5 py-2 text-xs text-muted-foreground"> | ||
| Unable to render Mermaid diagram. Showing source instead. | ||
| </div> | ||
| <pre className="min-w-0 px-3.5 py-2.5"><code className="block text-xs whitespace-pre">{source}</code></pre> | ||
| <CopyButton | ||
| text={source} | ||
| className="absolute right-1.5 top-1.5 h-8 w-8 text-muted-foreground opacity-0 transition-opacity group-hover/mermaid:opacity-100" | ||
| /> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| data-mermaid-diagram | ||
| aria-label="Rendering Mermaid diagram" | ||
| aria-busy="true" | ||
| className="my-3 h-40 max-w-full animate-pulse rounded-xl border border-border bg-muted/40" | ||
| /> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Workspace attachments always use
size: 0, which bypasses the classifier's existing large-JSON guard. JSON files above the preview threshold therefore open in the modal, are truncated by the text preview reader, and can no longer be parsed or formatted as JSON instead of following the large-file new-tab path. Obtain the actual file size before classification or classify workspace files without relying on a fabricated zero size.