Skip to content

feat: Add table support and syntax highlights for markdowns #3910

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

Merged
merged 4 commits into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
"react-i18next": "11.18.4",
"react-markdown": "8.0.3",
"react-router-dom": "^6.3.0",
"react-syntax-highlighter": "15.5.0",
"remark-gfm": "3.0.1",
"sourcemapped-stacktrace": "1.1.11",
"swr": "1.3.0",
"tzdata": "1.0.30",
Expand Down
2 changes: 1 addition & 1 deletion site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { NotFoundPage } from "./pages/404Page/404Page"
import { CliAuthenticationPage } from "./pages/CliAuthPage/CliAuthPage"
import { HealthzPage } from "./pages/HealthzPage/HealthzPage"
import { LoginPage } from "./pages/LoginPage/LoginPage"
import { TemplatePage } from "./pages/TemplatePage/TemplatePage"
import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"
import { AccountPage } from "./pages/UserSettingsPage/AccountPage/AccountPage"
import { SecurityPage } from "./pages/UserSettingsPage/SecurityPage/SecurityPage"
Expand All @@ -34,6 +33,7 @@ const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
const WorkspacesPage = lazy(() => import("./pages/WorkspacesPage/WorkspacesPage"))
const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"))
const AuditPage = lazy(() => import("./pages/AuditPage/AuditPage"))
const TemplatePage = lazy(() => import("./pages/TemplatePage/TemplatePage"))

export const AppRouter: FC = () => {
const xServices = useContext(XServiceContext)
Expand Down
73 changes: 73 additions & 0 deletions site/src/components/Markdown/Markdown.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ComponentMeta, Story } from "@storybook/react"
import { Markdown, MarkdownProps } from "./Markdown"

export default {
title: "components/Markdown",
component: Markdown,
} as ComponentMeta<typeof Markdown>

const Template: Story<MarkdownProps> = ({ children }) => <Markdown>{children}</Markdown>

export const WithCode = Template.bind({})
WithCode.args = {
children: `
## Required permissions / policy

The following sample policy allows Coder to create EC2 instances and modify instances provisioned by Coder:

\`\`\`json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:GetDefaultCreditSpecification",
"ec2:DescribeIamInstanceProfileAssociations",
"ec2:DescribeTags",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstanceCreditSpecifications",
"ec2:DescribeImages",
"ec2:ModifyDefaultCreditSpecification",
"ec2:DescribeVolumes"
],
"Resource": "*"
},
{
"Sid": "CoderResources",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeInstanceAttribute",
"ec2:UnmonitorInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:DeleteTags",
"ec2:MonitorInstances",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:ModifyInstanceAttribute",
"ec2:ModifyInstanceCreditSpecification"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/Coder_Provisioned": "true"
}
}
}
]
}
\`\`\``,
}

export const WithTable = Template.bind({})
WithTable.args = {
children: `
| heading | b | c | d |
| - | :- | -: | :-: |
| cell 1 | cell 2 | 3 | 4 | `,
}
105 changes: 105 additions & 0 deletions site/src/components/Markdown/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Link from "@material-ui/core/Link"
import { makeStyles, Theme, useTheme } from "@material-ui/core/styles"
import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableContainer from "@material-ui/core/TableContainer"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import { FC } from "react"
import ReactMarkdown from "react-markdown"
import SyntaxHighlighter from "react-syntax-highlighter"
import { dracula as dark } from "react-syntax-highlighter/dist/cjs/styles/hljs"
import gfm from "remark-gfm"

export interface MarkdownProps {
children: string
}

export const Markdown: FC<{ children: string }> = ({ children }) => {
const theme: Theme = useTheme()
const styles = useStyles()

return (
<ReactMarkdown
remarkPlugins={[gfm]}
components={{
a: ({ href, target, children }) => (
<Link href={href} target={target}>
{children}
</Link>
),

// Adding node so the ...props don't have it
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Comment on lines +33 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

❓ help me understand this (sorry if it's obvious 😂) we destructure node but don't use it, how come?

does prefixing with _ fix the ESLint warning?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

we destructure node but don't use it, how come?

Because we are getting the ...props and we don't want props having node.

does prefixing with _ fix the ESLint warning?

Yes, but if I use _node it will return _node not found because _node is not an attribute. I could do node: _node but I think it is more confusing than letting it as it is and adding an ignoring comment.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahhh okay, that makes sense! Thanks for explaining!

code: ({ node, inline, className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || "")
return !inline && match ? (
<SyntaxHighlighter
// Custom style to match our main colors
style={{
...dark,
hljs: {
...dark.hljs,
background: theme.palette.background.default,
borderRadius: theme.shape.borderRadius,
color: theme.palette.text.primary,
},
}}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, "")}
Copy link
Contributor

Choose a reason for hiding this comment

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

How come we have to do this .replace? Is it to remove line breaks or something?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it is so the highlight component can struct it properly. I got this from the react-markdown docs https://github.com/remarkjs/react-markdown#use-custom-components-syntax-highlight

</SyntaxHighlighter>
) : (
<code className={styles.codeWithoutLanguage} {...props}>
{children}
</code>
)
},

table: ({ children }) => {
return (
<TableContainer>
<Table>{children}</Table>
</TableContainer>
)
},

tr: ({ children }) => {
return <TableRow>{children}</TableRow>
},

thead: ({ children }) => {
return <TableHead>{children}</TableHead>
},

tbody: ({ children }) => {
return <TableBody>{children}</TableBody>
},

td: ({ children }) => {
return <TableCell>{children}</TableCell>
},

th: ({ children }) => {
return <TableCell>{children}</TableCell>
},
}}
>
{children}
</ReactMarkdown>
)
}

const useStyles = makeStyles((theme) => ({
codeWithoutLanguage: {
display: "block",
overflowX: "auto",
padding: "0.5em",
background: theme.palette.background.default,
borderRadius: theme.shape.borderRadius,
color: theme.palette.text.primary,
},
}))
2 changes: 2 additions & 0 deletions site/src/pages/TemplatePage/TemplatePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from "../../testHelpers/renderHelpers"
import { TemplatePage } from "./TemplatePage"

jest.mock("remark-gfm", () => jest.fn())

Object.defineProperty(window, "ResizeObserver", {
value: ResizeObserver,
})
Expand Down
2 changes: 2 additions & 0 deletions site/src/pages/TemplatePage/TemplatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,5 @@ export const TemplatePage: FC<React.PropsWithChildren<unknown>> = () => {
</>
)
}

export default TemplatePage
3 changes: 2 additions & 1 deletion site/src/pages/TemplatePage/TemplatePageView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ You can add instructions here
[Some link info](https://coder.com)
\`\`\`
# This is a really long sentence to test that the code block wraps into a new line properly.
\`\`\``,
\`\`\`
`,
},
templateResources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2],
templateVersions: [Mocks.MockTemplateVersion],
Expand Down
20 changes: 2 additions & 18 deletions site/src/pages/TemplatePage/TemplatePageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
import { DeleteButton } from "components/DropdownButton/ActionCtas"
import { DropdownButton } from "components/DropdownButton/DropdownButton"
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
import { Markdown } from "components/Markdown/Markdown"
import frontMatter from "front-matter"
import { FC } from "react"
import ReactMarkdown from "react-markdown"
import { Link as RouterLink } from "react-router-dom"
import { firstLetter } from "util/firstLetter"
import {
Expand Down Expand Up @@ -147,17 +147,7 @@ export const TemplatePageView: FC<React.PropsWithChildren<TemplatePageViewProps>
contentsProps={{ className: styles.readmeContents }}
>
<div className={styles.markdownWrapper}>
<ReactMarkdown
components={{
a: ({ href, target, children }) => (
<Link href={href} target={target}>
{children}
</Link>
),
}}
>
{readme.body}
</ReactMarkdown>
<Markdown>{readme.body}</Markdown>
</div>
</WorkspaceSection>
<WorkspaceSection
Expand All @@ -184,12 +174,6 @@ export const useStyles = makeStyles((theme) => {
markdownWrapper: {
background: theme.palette.background.paper,
padding: theme.spacing(3, 4),

// Adds text wrapping to <pre> tag added by ReactMarkdown
"& pre": {
whiteSpace: "pre-wrap",
wordWrap: "break-word",
},
},
versionsTableContents: {
margin: 0,
Expand Down
Loading