diff --git a/site/src/components/Markdown/Markdown.tsx b/site/src/components/Markdown/Markdown.tsx index 97f840ce6bd03..6a54941f5f7a6 100644 --- a/site/src/components/Markdown/Markdown.tsx +++ b/site/src/components/Markdown/Markdown.tsx @@ -5,13 +5,14 @@ import TableCell from "@mui/material/TableCell"; import TableContainer from "@mui/material/TableContainer"; import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; -import { FC, memo } from "react"; -import ReactMarkdown from "react-markdown"; +import { type Interpolation, type Theme } from "@emotion/react"; +import isEqual from "lodash/isEqual"; +import { type FC, memo } from "react"; +import ReactMarkdown, { type Options } from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import gfm from "remark-gfm"; import { colors } from "theme/colors"; import { darcula } from "react-syntax-highlighter/dist/cjs/styles/prism"; -import { type Interpolation, type Theme } from "@emotion/react"; interface MarkdownProps { /** @@ -20,10 +21,15 @@ interface MarkdownProps { children: string; className?: string; + + /** + * Can override the behavior of the generated elements + */ + components?: Options["components"]; } export const Markdown: FC = (props) => { - const { children, className } = props; + const { children, className, components = {} } = props; return ( = (props) => { th: ({ children }) => { return {children}; }, + + ...components, + }} + > + {children} + + ); +}; + +interface MarkdownInlineProps { + /** + * The Markdown text to parse and render + */ + children: string; + + className?: string; + + /** + * Can override the behavior of the generated elements + */ + components?: Options["components"]; +} + +/** + * Supports a strict subset of Markdown that bahaves well as inline/confined content. + */ +export const InlineMarkdown: FC = (props) => { + const { children, className, components = {} } = props; + + return ( + <>{children}, + + a: ({ href, target, children }) => ( + + {children} + + ), + + code: ({ node, className, children, style, ...props }) => ( + ({ + padding: "1px 4px", + background: theme.palette.divider, + borderRadius: 4, + color: theme.palette.text.primary, + fontSize: 14, + })} + {...props} + > + {children} + + ), + + ...components, }} > {children} @@ -113,7 +178,8 @@ export const Markdown: FC = (props) => { ); }; -export const MemoizedMarkdown = memo(Markdown); +export const MemoizedMarkdown = memo(Markdown, isEqual); +export const MemoizedInlineMarkdown = memo(InlineMarkdown, isEqual); const markdownStyles: Interpolation = (theme: Theme) => ({ fontSize: 16, diff --git a/site/src/components/Resources/ResourceCard.tsx b/site/src/components/Resources/ResourceCard.tsx index 2d327c99760e0..b3f754d867cb6 100644 --- a/site/src/components/Resources/ResourceCard.tsx +++ b/site/src/components/Resources/ResourceCard.tsx @@ -1,10 +1,12 @@ -import { type FC, useState } from "react"; +import { type FC, type PropsWithChildren, useState } from "react"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; import { type CSSObject, type Interpolation, type Theme } from "@emotion/react"; +import { Children } from "react"; import type { WorkspaceAgent, WorkspaceResource } from "api/typesGenerated"; import { DropdownArrow } from "../DropdownArrow/DropdownArrow"; import { CopyableValue } from "../CopyableValue/CopyableValue"; +import { MemoizedInlineMarkdown } from "../Markdown/Markdown"; import { Stack } from "../Stack/Stack"; import { ResourceAvatar } from "./ResourceAvatar"; import { SensitiveValue } from "./SensitiveValue"; @@ -72,6 +74,14 @@ export interface ResourceCardProps { agentRow: (agent: WorkspaceAgent) => JSX.Element; } +const p = ({ children }: PropsWithChildren) => { + const childrens = Children.toArray(children); + if (childrens.every((child) => typeof child === "string")) { + return {children}; + } + return <>{children}; +}; + export const ResourceCard: FC = ({ resource, agentRow }) => { const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] = useState(false); @@ -136,9 +146,9 @@ export const ResourceCard: FC = ({ resource, agentRow }) => { {meta.sensitive ? ( ) : ( - + {meta.value} - + )} diff --git a/site/src/components/Resources/Resources.stories.tsx b/site/src/components/Resources/Resources.stories.tsx index 7e107b79ad315..32cde805cfcbe 100644 --- a/site/src/components/Resources/Resources.stories.tsx +++ b/site/src/components/Resources/Resources.stories.tsx @@ -58,6 +58,44 @@ const reallyLong = { sensitive: false, }; +export const Markdown: Story = { + args: { + resources: [ + { + ...nullDevice, + type: "workspace", + id: "1", + name: "Workspace", + metadata: [ + { key: "text", value: "hello", sensitive: false }, + { key: "link", value: "[hello](#)", sensitive: false }, + { key: "b/i", value: "_hello_, **friend**!", sensitive: false }, + { key: "coder", value: "`beep boop`", sensitive: false }, + ], + }, + + // bits of Markdown that are intentionally not supported here + { + ...nullDevice, + type: "unsupported", + id: "2", + name: "Unsupported", + metadata: [ + { + key: "multiple paragraphs", + value: `home, + +home on the range`, + sensitive: false, + }, + { key: "heading", value: "# HI", sensitive: false }, + { key: "image", value: "![go](/icon/go.svg)", sensitive: false }, + ], + }, + ], + }, +}; + export const BunchOfDevicesWithMetadata: Story = { args: { resources: [