Skip to content

Commit

Permalink
feat: Added DSN selector to code blocks
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed May 8, 2020
1 parent 7bbe850 commit 727f501
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 19 deletions.
7 changes: 2 additions & 5 deletions gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const getPlugins = () => {
remarkPlugins: [require("remark-deflist")],
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-copy-linked-files`
resolve: `gatsby-remark-copy-linked-files`,
},
{
resolve: `gatsby-remark-autolink-headers`,
Expand All @@ -35,14 +35,11 @@ const getPlugins = () => {
resolve: `gatsby-remark-images`,
options: {
maxWidth: 1200,
linkImagesToOriginal: false
linkImagesToOriginal: false,
},
},
{
resolve: require.resolve("./plugins/gatsby-plugin-code-tabs"),
options: {
githubRepo: "getsentry/develop"
}
},
{
resolve: "gatsby-remark-prismjs",
Expand Down
168 changes: 165 additions & 3 deletions src/components/codeBlock.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,159 @@
import React, { useState, useRef } from "react";
import React, { useState, useRef, useContext } from "react";
import PropTypes from "prop-types";
import copy from "copy-to-clipboard";
import { MDXProvider } from "@mdx-js/react";
import { useOnClickOutside, useRefWithCallback } from "../utils";
import { CodeContext } from "./codeTabs";

const KEYWORDS_REGEX = /\b___([A-Z_][A-Z0-9_]*)\.([A-Z_][A-Z0-9_]*)___\b/g;

function makeKeywordsClickable(children) {
if (!Array.isArray(children)) {
children = [children];
}
KEYWORDS_REGEX.lastIndex = 0;

return children.reduce((arr, child) => {
if (typeof child !== "string") {
arr.push(child);
return arr;
}

let match;
let lastIndex = 0;
while ((match = KEYWORDS_REGEX.exec(child)) !== null) {
let afterMatch = KEYWORDS_REGEX.lastIndex - match[0].length;
let before = child.substring(lastIndex, afterMatch);
if (before.length > 0) {
arr.push(before);
}
arr.push(
Selector({
group: match[1],
keyword: match[2],
key: lastIndex,
})
);
lastIndex = KEYWORDS_REGEX.lastIndex;
}

let after = child.substr(lastIndex);
if (after.length > 0) {
arr.push(after);
}

return arr;
}, []);
}

function Selector({ keyword, group, ...props }) {
const [isOpen, setIsOpen] = useState(false);
const codeContext = useContext(CodeContext);
const [
sharedSelection,
setSharedSelection,
] = codeContext.sharedKeywordSelection;
const spanRef = useRef();
const [menuRef, setMenuRef] = useRefWithCallback((menuNode) => {
if (menuNode) {
for (const node of menuNode.childNodes) {
if (node.getAttribute("data-active") === "1") {
node.parentNode.scrollTop =
node.offsetTop -
node.parentNode.getBoundingClientRect().height / 2 +
node.getBoundingClientRect().height / 2;
break;
}
}
}
});

useOnClickOutside(menuRef, () => {
if (isOpen) {
setIsOpen(false);
}
});

const { codeKeywords } = useContext(CodeContext);
const choices = (codeKeywords && codeKeywords[group]) || [];
const currentSelectionIdx = sharedSelection[group] || 0;
const currentSelection = choices[currentSelectionIdx];

if (!currentSelection) {
return null;
}

// this is not super clean but since we can depend on the span
// rendering before the menu this works.
const style = {};
if (spanRef.current) {
const rect = spanRef.current.getBoundingClientRect();
style.left = spanRef.current.offsetLeft - 10 + "px";
style.top = spanRef.current.offsetTop + 20 + "px";
style.minWidth = rect.width + 20 + "px";
}

return (
<span className="keyword-selector-wrapper" {...props}>
<span
ref={spanRef}
className={`keyword-selector ${isOpen ? " open" : ""}`}
title={currentSelection && currentSelection.title}
onClick={() => {
setIsOpen(!isOpen);
}}
>
{currentSelection[keyword]}
</span>
{isOpen && (
<div style={style} className="selections" ref={setMenuRef}>
{choices.map((item, idx) => {
const isActive = idx === currentSelectionIdx;
return (
<button
key={idx}
data-active={isActive ? "1" : ""}
onClick={() => {
let newSharedSelection = { ...sharedSelection };
newSharedSelection[group] = idx;
setSharedSelection(newSharedSelection);
setIsOpen(false);
}}
className={isActive ? "active" : ""}
>
{item.title}
</button>
);
})}
</div>
)}
</span>
);
}

function CodeWrapper(props) {
let { children, class: className, ...rest } = props;
if (children) {
children = makeKeywordsClickable(children);
}
return (
<code className={className} {...rest}>
{children}
</code>
);
}

function SpanWrapper(props) {
let { children, class: className, ...rest } = props;
if (children) {
children = makeKeywordsClickable(children);
}
return (
<span className={className} {...rest}>
{children}
</span>
);
}

function CodeBlock({ filename, children }) {
const [showCopied, setShowCopied] = useState(false);
Expand All @@ -14,15 +167,24 @@ function CodeBlock({ filename, children }) {

return (
<div className="code-block">
{filename && <p class="filename">{filename}</p>}
{filename && <p className="filename">{filename}</p>}
{showCopied ? (
<button className="copied">Copied!</button>
) : (
<button className="copy" onClick={() => copyCode()}>
Copy
</button>
)}
<div ref={codeRef}>{children}</div>
<div ref={codeRef}>
<MDXProvider
components={{
code: CodeWrapper,
span: SpanWrapper,
}}
>
{children}
</MDXProvider>
</div>
</div>
);
}
Expand Down
26 changes: 23 additions & 3 deletions src/components/codeTabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,27 @@ const LANGUAGES = {

export const CodeContext = React.createContext(null);

export function useCodeContextState() {
return useState(null);
// only fetch them once
let cachedCodeKeywords = null;

export function useCodeContextState(fetcher) {
let [codeKeywords, setCodeKeywords] = useState(null);
if (codeKeywords === null) {
if (cachedCodeKeywords) {
setCodeKeywords(cachedCodeKeywords);
codeKeywords = cachedCodeKeywords;
} else {
fetcher().then((config) => {
cachedCodeKeywords = config;
setCodeKeywords(config);
});
}
}
return {
codeKeywords,
sharedCodeSelection: useState(null),
sharedKeywordSelection: useState({}),
};
}

function CodeTabs({ children, hideTabBar = false }) {
Expand All @@ -42,7 +61,8 @@ function CodeTabs({ children, hideTabBar = false }) {
// individual code block. In that case the local selection overrides. The
// final selection is what is then rendered.

const [sharedSelection, setSharedSelection] = useContext(CodeContext);
const codeContext = useContext(CodeContext);
const [sharedSelection, setSharedSelection] = codeContext.sharedCodeSelection;
const [localSelection, setLocalSelection] = useState(null);
const [lastScrollOffset, setLastScrollOffset] = useState(null);
const tabBarRef = useRef(null);
Expand Down
35 changes: 34 additions & 1 deletion src/components/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,37 @@ const TableOfContents = ({ toc: { items } }) => {
return <ul className="section-nav">{recurseyMcRecurseFace(items)}</ul>;
};

function fetchCodeKeywords() {
return fetch("https://sentry.io/docs/api/user/")
.catch(() => false)
.then((result) => (!result ? { projects: [] } : result.json()))
.then(({ projects }) => {
if (projects.length === 0) {
projects.push({
publicKey: "e24732883e6fcdad45fd27341e61f8899227bb39",
dsnPublic:
"https://[email protected]/42",
id: 42,
organizationName: "Example Org",
organizationId: 43,
organizationSlug: "example-org",
projectSlug: "example-project",
});
}
return {
PROJECT: projects.map((project) => {
return {
DSN: project.dsnPublic,
ID: project.id,
SLUG: project.projectSlug,
ORG_SLUG: project.organizationSlug,
title: `${project.organizationName} / ${project.projectSlug}`,
};
}),
};
});
}

const GitHubCTA = ({ sourceInstanceName, relativePath }) => (
<div className="github-cta">
<small>
Expand Down Expand Up @@ -100,7 +131,9 @@ const Layout = ({
>
<h1 className="mb-3">{mdx.frontmatter.title}</h1>
<div id="main">
<CodeContext.Provider value={useCodeContextState()}>
<CodeContext.Provider
value={useCodeContextState(fetchCodeKeywords)}
>
<MDXProvider components={mdxComponents}>
<MDXRenderer>{mdx.body}</MDXRenderer>
</MDXProvider>
Expand Down
54 changes: 54 additions & 0 deletions src/css/_includes/code-blocks.scss
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,60 @@
display: none;
}

.keyword-selector {
background: $desatPurple4;
border-radius: 2px;
margin: 0 -2px;
padding: 0 2px;
cursor: context-menu;

&:after {
content: "";
}

&.open:after {
content: "";
}

&:hover {
background: $desatPurple3;

&:after {
color: white;
}
}
}

.keyword-selector-wrapper {
.selections {
margin-top: -2px;
position: absolute;
background: $desatPurple6;
color: white;
border-radius: 3px;
box-shadow: 0px 18px 46px -11px rgba(0, 0, 0, 0.63);
overflow: auto;
max-height: 200px;
z-index: 100;

button {
font-family: $font-family-sans-serif;
padding: 2px 8px;
text-align: left;
display: block;
width: 100%;
background: none;
color: white;
border: none;

&:hover,
&.active {
background: $desatPurple3;
}
}
}
}

pre {
margin: 0;
padding: 5px 10px;
Expand Down
13 changes: 9 additions & 4 deletions src/docs/docs-components.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ the `<Break/>` component.
Example of two code blocks merged:

```python
print "Hello World!"
import sentry_sdk
sentry_sdk.init("___PROJECT.DSN___");
```

```javascript
console.log("Hello World!")
import * as Sentry from "sentry-browser";
Sentry.init("___PROJECT.DSN___");
// id: ___PROJECT.ID___
// org-slug: ___PROJECT.ORG_SLUG___
// slug: ___PROJECT.SLUG___
```

A second example with three code blocks that have file names set:
Expand Down Expand Up @@ -109,11 +114,11 @@ Example usage in a nested structure:
1. Creation of the SDK (sometimes this is hidden from the user):

```javascript
Sentry.init({dsn: '___DSN___'});
Sentry.init({dsn: '___PROJECT.DSN___'});
```

```python
sentry_sdk.init('___DSN___')
sentry_sdk.init('___PROJECT.DSN___')
```

```bash
Expand Down
Loading

0 comments on commit 727f501

Please sign in to comment.