mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: parse resource metadata values as markdown (#10521)
This commit is contained in:
@@ -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<MarkdownProps> = (props) => {
|
||||
const { children, className } = props;
|
||||
const { children, className, components = {} } = props;
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
@@ -106,6 +112,8 @@ export const Markdown: FC<MarkdownProps> = (props) => {
|
||||
th: ({ children }) => {
|
||||
return <TableCell>{children}</TableCell>;
|
||||
},
|
||||
|
||||
...components,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -113,7 +121,65 @@ export const Markdown: FC<MarkdownProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const MemoizedMarkdown = memo(Markdown);
|
||||
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<MarkdownInlineProps> = (props) => {
|
||||
const { children, className, components = {} } = props;
|
||||
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className={className}
|
||||
allowedElements={["p", "em", "strong", "a", "pre", "code"]}
|
||||
unwrapDisallowed
|
||||
components={{
|
||||
p: ({ children }) => <>{children}</>,
|
||||
|
||||
a: ({ href, target, children }) => (
|
||||
<Link href={href} target={target}>
|
||||
{children}
|
||||
</Link>
|
||||
),
|
||||
|
||||
code: ({ node, className, children, style, ...props }) => (
|
||||
<code
|
||||
css={(theme) => ({
|
||||
padding: "1px 4px",
|
||||
background: theme.palette.divider,
|
||||
borderRadius: 4,
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: 14,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
|
||||
...components,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
};
|
||||
|
||||
export const MemoizedMarkdown = memo(Markdown, isEqual);
|
||||
export const MemoizedInlineMarkdown = memo(InlineMarkdown, isEqual);
|
||||
|
||||
const markdownStyles: Interpolation<Theme> = (theme: Theme) => ({
|
||||
fontSize: 16,
|
||||
|
||||
@@ -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 <CopyableValue value={childrens.join("")}>{children}</CopyableValue>;
|
||||
}
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const ResourceCard: FC<ResourceCardProps> = ({ resource, agentRow }) => {
|
||||
const [shouldDisplayAllMetadata, setShouldDisplayAllMetadata] =
|
||||
useState(false);
|
||||
@@ -136,9 +146,9 @@ export const ResourceCard: FC<ResourceCardProps> = ({ resource, agentRow }) => {
|
||||
{meta.sensitive ? (
|
||||
<SensitiveValue value={meta.value} />
|
||||
) : (
|
||||
<CopyableValue value={meta.value}>
|
||||
<MemoizedInlineMarkdown components={{ p }}>
|
||||
{meta.value}
|
||||
</CopyableValue>
|
||||
</MemoizedInlineMarkdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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: "", sensitive: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const BunchOfDevicesWithMetadata: Story = {
|
||||
args: {
|
||||
resources: [
|
||||
|
||||
Reference in New Issue
Block a user