-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Adding interactive Connect functionality to Connect docs #16414
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
Changes from all commits
fe072c6
6dc6146
0355dbd
bdbd4a6
c763043
7629ea6
7494f47
11ea367
d7c9c58
44efcc1
0b31b6f
8b08d2d
a5f79f3
b156115
67db40f
6186932
fd8b9ef
9d365c8
df2a700
e80ca0b
50356d8
27ef904
21afe03
acaac74
064a246
8be5e39
9434f9a
8ca823a
fc87685
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # Connect demo API configuration | ||
| PIPEDREAM_CLIENT_ID=your_client_id | ||
| PIPEDREAM_CLIENT_SECRET=your_client_secret | ||
| PIPEDREAM_PROJECT_ID=your_project_id | ||
| PIPEDREAM_PROJECT_ENVIRONMENT=development | ||
|
|
||
| # Additional redirect URIs for the Connect demo (optional) | ||
| PIPEDREAM_CONNECT_TOKEN_WEBHOOK_URI= | ||
| PIPEDREAM_CONNECT_SUCCESS_REDIRECT_URI= | ||
| PIPEDREAM_CONNECT_ERROR_REDIRECT_URI= | ||
|
|
||
| # Comma-separated list of additional allowed origins (optional) | ||
| # ALLOWED_ORIGINS=https://your-custom-domain.com,https://another-domain.com |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| "use client"; | ||
|
|
||
| import { useGlobalConnect } from "./GlobalConnectProvider"; | ||
| import CodeBlock from "./CodeBlock"; | ||
| import { styles } from "../utils/componentStyles"; | ||
|
|
||
| export default function AccountConnectionDemo() { | ||
| const { | ||
| appSlug, | ||
| setAppSlug, | ||
| tokenData, | ||
| getClientCodeSnippet, | ||
| connectAccount, | ||
| connectedAccount, | ||
| error, | ||
| } = useGlobalConnect(); | ||
|
|
||
| return ( | ||
| <div className={styles.container}> | ||
| <div className={styles.header}> | ||
| Connect an account from your frontend | ||
| </div> | ||
| <div className="p-4"> | ||
| <div className="mb-4"> | ||
| <label className="flex items-center mb-4"> | ||
| <span className={styles.label}>App to connect:</span> | ||
| <select | ||
| value={appSlug} | ||
| onChange={(e) => setAppSlug(e.target.value)} | ||
| className={styles.select} | ||
| > | ||
| <option value="google_sheets">Google Sheets</option> | ||
| <option value="github">GitHub</option> | ||
| <option value="notion">Notion</option> | ||
| <option value="gmail">Gmail</option> | ||
| <option value="openai">OpenAI</option> | ||
| </select> | ||
| </label> | ||
|
|
||
| <div className="mb-4"> | ||
| <div className="border border-blue-100 rounded-lg overflow-hidden"> | ||
| <CodeBlock code={getClientCodeSnippet()} language="javascript" /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="mt-4 mb-2"> | ||
| <button | ||
| onClick={connectAccount} | ||
| disabled={!tokenData} | ||
| className={styles.primaryButton} | ||
| > | ||
| Connect Account | ||
| </button> | ||
| {!tokenData && <p className={`mt-2 ${styles.text.muted}`}><a href="/docs/connect/managed-auth/quickstart/#generate-a-short-lived-token" className="font-semibold underline underline-offset-4 hover:decoration-2 decoration-brand/50">Generate a token above</a> in order to test the account connection flow</p>} | ||
| </div> | ||
|
|
||
| {error && ( | ||
| <div className={styles.statusBox.error}> | ||
| <div className="font-medium text-sm">Error</div> | ||
| <div className="mt-1 text-sm">{error}</div> | ||
| </div> | ||
| )} | ||
|
|
||
| {connectedAccount && ( | ||
| <div className={styles.statusBox.success}> | ||
| <div className="font-medium text-sm">Successfully connected your {appSlug} account!</div> | ||
| <div className="mt-4 text-sm"> | ||
| {connectedAccount.loading | ||
| ? ( | ||
| <div>Loading account details...</div> | ||
| ) | ||
| : ( | ||
| <> | ||
| {connectedAccount.name | ||
| ? ( | ||
| <div>Account info: <span className="font-medium">{connectedAccount.name}</span></div> | ||
| ) | ||
| : null} | ||
| <div>Account ID: <span className="font-medium">{connectedAccount.id}</span></div> | ||
| </> | ||
| )} | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,109 @@ | ||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
| useState, useEffect, | ||||||||||||||||||||||||||||||||||||||||
| } from "react"; | ||||||||||||||||||||||||||||||||||||||||
| // We don't need the default Prism CSS as we're using our custom CSS | ||||||||||||||||||||||||||||||||||||||||
| // import "prismjs/themes/prism.css"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // We'll dynamically import Prism on the client side only | ||||||||||||||||||||||||||||||||||||||||
| let Prism; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export default function CodeBlock({ | ||||||||||||||||||||||||||||||||||||||||
| code, language = "javascript", className = "", | ||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||
| const [ | ||||||||||||||||||||||||||||||||||||||||
| copied, | ||||||||||||||||||||||||||||||||||||||||
| setCopied, | ||||||||||||||||||||||||||||||||||||||||
| ] = useState(false); | ||||||||||||||||||||||||||||||||||||||||
| const [ | ||||||||||||||||||||||||||||||||||||||||
| highlightedCode, | ||||||||||||||||||||||||||||||||||||||||
| setHighlightedCode, | ||||||||||||||||||||||||||||||||||||||||
| ] = useState(code); | ||||||||||||||||||||||||||||||||||||||||
| const [ | ||||||||||||||||||||||||||||||||||||||||
| isClient, | ||||||||||||||||||||||||||||||||||||||||
| setIsClient, | ||||||||||||||||||||||||||||||||||||||||
| ] = useState(false); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Load Prism and highlight code on client-side only | ||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||
| setIsClient(true); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const loadPrism = async () => { | ||||||||||||||||||||||||||||||||||||||||
| Prism = (await import("prismjs")).default; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Use manual mode so we can control highlighting | ||||||||||||||||||||||||||||||||||||||||
| Prism.manual = true; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Import language definitions dynamically | ||||||||||||||||||||||||||||||||||||||||
| if (!Prism.languages.javascript) { | ||||||||||||||||||||||||||||||||||||||||
| await import("prismjs/components/prism-javascript"); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (!Prism.languages.json && language === "json") { | ||||||||||||||||||||||||||||||||||||||||
| await import("prismjs/components/prism-json"); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Apply syntax highlighting | ||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||
| if (Prism.languages[language]) { | ||||||||||||||||||||||||||||||||||||||||
| const highlighted = Prism.highlight(code, Prism.languages[language], language); | ||||||||||||||||||||||||||||||||||||||||
| setHighlightedCode(highlighted); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||
| console.error("Prism highlighting error:", error); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| loadPrism(); | ||||||||||||||||||||||||||||||||||||||||
| }, [ | ||||||||||||||||||||||||||||||||||||||||
| code, | ||||||||||||||||||||||||||||||||||||||||
| language, | ||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const copyToClipboard = () => { | ||||||||||||||||||||||||||||||||||||||||
| navigator.clipboard.writeText(code); | ||||||||||||||||||||||||||||||||||||||||
| setCopied(true); | ||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => setCopied(false), 2000); | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+68
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. 🛠️ Refactor suggestion Add error handling for clipboard operations. The clipboard API might fail in certain browsers or contexts, but there's no error handling in the current implementation. const copyToClipboard = () => {
- navigator.clipboard.writeText(code);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
+ navigator.clipboard.writeText(code)
+ .then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ })
+ .catch(err => {
+ console.error("Failed to copy code:", err);
+ // Optionally show a user-friendly error message
+ });
};This handles potential errors and makes the component more robust. 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <div className={`relative group ${className}`}> | ||||||||||||||||||||||||||||||||||||||||
| <pre className="overflow-x-auto rounded-lg font-medium border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 p-4 text-[13px] leading-relaxed mb-0"> | ||||||||||||||||||||||||||||||||||||||||
| <div className="absolute top-2 right-2 z-10"> | ||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||
| onClick={copyToClipboard} | ||||||||||||||||||||||||||||||||||||||||
| className="opacity-0 group-hover:opacity-100 transition-opacity h-8 w-8 rounded-md bg-gray-100 dark:bg-gray-800 flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-gray-700 dark:hover:text-gray-300 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-blue-500" | ||||||||||||||||||||||||||||||||||||||||
| aria-label={copied | ||||||||||||||||||||||||||||||||||||||||
| ? "Copied" | ||||||||||||||||||||||||||||||||||||||||
| : "Copy code"} | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| {copied | ||||||||||||||||||||||||||||||||||||||||
| ? ( | ||||||||||||||||||||||||||||||||||||||||
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||||||||||||||||||||||||||||||||||||
| <path d="M5 13l4 4L19 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> | ||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| : ( | ||||||||||||||||||||||||||||||||||||||||
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||||||||||||||||||||||||||||||||||||
| <path d="M8 16c0 1.886 0 2.828.586 3.414C9.172 20 10.114 20 12 20h4c1.886 0 2.828 0 3.414-.586C20 18.828 20 17.886 20 16v-4c0-1.886 0-2.828-.586-3.414C18.828 8 17.886 8 16 8m-8 8h4c1.886 0 2.828 0 3.414-.586C16 14.828 16 13.886 16 12V8m-8 8c-1.886 0-2.828 0-3.414-.586C4 14.828 4 13.886 4 12V8c0-1.886 0-2.828.586-3.414C5.172 4 6.114 4 8 4h4c1.886 0 2.828 0 3.414.586C16 5.172 16 6.114 16 8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> | ||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| {isClient | ||||||||||||||||||||||||||||||||||||||||
| ? ( | ||||||||||||||||||||||||||||||||||||||||
| <code | ||||||||||||||||||||||||||||||||||||||||
| className={`language-${language} text-gray-800 dark:text-gray-200 [text-shadow:none]`} | ||||||||||||||||||||||||||||||||||||||||
| dangerouslySetInnerHTML={{ | ||||||||||||||||||||||||||||||||||||||||
| __html: highlightedCode, | ||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+98
to
+100
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. 💡 Verification agent 🧩 Analysis chainConsider XSS protection when using dangerouslySetInnerHTML. While necessary for syntax highlighting, using dangerouslySetInnerHTML can expose users to XSS attacks if the input isn't properly sanitized. Ensure the 🌐 Web query: 💡 Result: Using dangerouslySetInnerHTML with PrismJS in React SafelyWhen combining PrismJS for syntax highlighting with React components, you'll often need to use Understanding the RisksThe Best Practices for Safe Implementation1. Sanitize Input ContentAlways sanitize any HTML content before passing it to Use established sanitization libraries:
Example implementation with DOMPurify: import DOMPurify from 'dompurify';
import Prism from 'prismjs';
import 'prismjs/themes/prism.css';
function CodeBlock({ code, language }) {
const highlightedCode = Prism.highlight(
code,
Prism.languages[language] || Prism.languages.javascript,
language
);
const sanitizedCode = DOMPurify.sanitize(highlightedCode);
return (
<pre>
<code
className={`language-${language}`}
dangerouslySetInnerHTML={{ __html: sanitizedCode }}
/>
</pre>
);
}2. Proper PrismJS IntegrationThere are two main approaches to integrating PrismJS with React: Approach 1: Using marked with PrismJS for Markdownimport React, { useEffect } from 'react';
import Prism from 'prismjs';
import 'prismjs/themes/prism.css';
import { marked } from 'marked';
function MarkdownRenderer({ markdownText }) {
// Configure marked to use Prism for highlighting
marked.setOptions({
breaks: true,
highlight: function (code, language) {
if (Prism.languages[language]) {
return Prism.highlight(code, Prism.languages[language], language);
}
return code;
}
});
// Create renderer for additional customization if needed
const renderer = new marked.Renderer();
return (
<div
className="markdown-content"
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(marked(markdownText, { renderer }))
}}
/>
);
}This approach is particularly useful when you need to render markdown content with code blocks that require syntax highlighting[5]. Approach 2: Direct PrismJS HighlightingFor cases where you need to highlight specific code snippets: import React, { useEffect, useRef } from 'react';
import Prism from 'prismjs';
import 'prismjs/themes/prism.css';
function CodeHighlighter({ code, language }) {
const codeRef = useRef(null);
useEffect(() => {
if (codeRef.current) {
Prism.highlightElement(codeRef.current);
}
}, [code, language]);
return (
<pre>
<code ref={codeRef} className={`language-${language}`}>
{code}
</code>
</pre>
);
}This approach avoids 3. Timing ConsiderationsOne common issue with PrismJS in React is timing problems, where highlighting occurs before React finishes rendering. To solve this: function CodeBlock({ code, language }) {
const codeRef = useRef(null);
useEffect(() => {
// Ensure highlighting happens after React rendering is complete
if (codeRef.current) {
Prism.highlightElement(codeRef.current);
}
}, [code, language]);
return (
<pre>
<code
ref={codeRef}
className={`language-${language}`}
>
{code}
</code>
</pre>
);
}This approach addresses the potential mismatch between React's rendering cycle and when Prism tries to highlight the code[1]. Alternative ApproachesUsing Unified/Remark/RehypeFor more complex markdown processing with syntax highlighting, consider using the unified ecosystem: import React from 'react';
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypePrism from 'rehype-prism';
import rehypeReact from 'rehype-react';
function MarkdownWithSyntaxHighlighting({ markdown }) {
const content = unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypePrism)
.use(rehypeReact, { createElement: React.createElement })
.processSync(markdown).result;
return <div className="markdown-content">{content}</div>;
}This approach avoids When to Avoid dangerouslySetInnerHTMLConsider alternatives to
In these cases, approaches like the unified/remark/rehype pipeline or direct DOM manipulation through refs might be safer options. ConclusionWhile
By following these best practices, you can safely implement code syntax highlighting in your React applications while maintaining robust security protections. Citations:
Sanitize highlighted code before using dangerouslySetInnerHTML Using • Import and apply DOMPurify to your highlighted HTML: import React from "react";
+import DOMPurify from "dompurify";
// …
<code
className={`language-${language}`}
- dangerouslySetInnerHTML={{ __html: highlightedCode }}
+ dangerouslySetInnerHTML={{
+ __html: DOMPurify.sanitize(highlightedCode),
+ }}
/>• Alternatively, avoid import React, { useEffect, useRef } from "react";
import Prism from "prismjs";
function CodeBlock({ code, language }) {
const codeRef = useRef(null);
useEffect(() => {
if (codeRef.current) Prism.highlightElement(codeRef.current);
}, [code, language]);
return (
<pre>
<code ref={codeRef} className={`language-${language}`}>
{code}
</code>
</pre>
);
}Select one of these approaches to ensure your code snippets remain XSS-safe. 🧰 Tools🪛 Biome (1.9.4)[error] 97-97: Avoid passing content using the dangerouslySetInnerHTML prop. Setting content using code can expose users to cross-site scripting (XSS) attacks (lint/security/noDangerouslySetInnerHtml) |
||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| : ( | ||||||||||||||||||||||||||||||||||||||||
| <code className={`language-${language} text-gray-800 dark:text-gray-200 [text-shadow:none]`}>{code}</code> | ||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||
| </pre> | ||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /** | ||
| * This file contains all the code snippets used in the Connect SDK demo. | ||
| * Centralizing them here helps to maintain consistency and makes updates easier. | ||
| */ | ||
|
|
||
| /** | ||
| * Server-side code for generating a Connect token | ||
| * @param {string} externalUserId - The user's external ID | ||
| * @returns {string} The server code snippet | ||
| */ | ||
| export function getServerCodeSnippet(externalUserId) { | ||
| return `import { createBackendClient } from "@pipedream/sdk/server"; | ||
|
|
||
| // This code runs on your server | ||
| const pd = createBackendClient({ | ||
| environment: "development", | ||
| credentials: { | ||
| clientId: process.env.PIPEDREAM_CLIENT_ID, | ||
| clientSecret: process.env.PIPEDREAM_CLIENT_SECRET, | ||
| }, | ||
| projectId: process.env.PIPEDREAM_PROJECT_ID | ||
| }); | ||
|
|
||
| // Create a token for a specific user | ||
| const { token, expires_at, connect_link_url } = await pd.createConnectToken({ | ||
| external_user_id: "${externalUserId || "YOUR_USER_ID"}", | ||
| });`; | ||
| } | ||
|
|
||
| /** | ||
| * Client-side code for connecting an account | ||
| * @param {string} appSlug - The app to connect to (slack, github, etc) | ||
| * @param {object} tokenData - The token data from the server | ||
| * @returns {string} The client code snippet | ||
| */ | ||
| export function getClientCodeSnippet(appSlug, tokenData) { | ||
| return `import { createFrontendClient } from "@pipedream/sdk/browser" | ||
|
|
||
| // This code runs in the frontend using the token from your server | ||
| export default function Home() { | ||
| function connectAccount() { | ||
| const pd = createFrontendClient() | ||
| pd.connectAccount({ | ||
| app: "${appSlug}", | ||
| token: "${tokenData?.token | ||
| ? tokenData.token | ||
| : "{connect_token}"}", | ||
| onSuccess: (account) => { | ||
| // Handle successful connection | ||
| console.log(\`Account successfully connected: \${account.id}\`) | ||
| }, | ||
| onError: (err) => { | ||
| // Handle connection error | ||
| console.error(\`Connection error: \${err.message}\`) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| return ( | ||
| <main> | ||
| <button onClick={connectAccount}>Connect Account</button> | ||
| </main> | ||
| ) | ||
| }`; | ||
| } |
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.
Default appSlug (
"slack") is missing from the<select>listGlobalConnectProviderinitialisesappSlugwith"slack", but the option is not rendered here.On first render the
<select>will show a blank value, forcing the user to re-select an app.<select value={appSlug} onChange={(e) => setAppSlug(e.target.value)} className="ml-2 p-1 border rounded text-sm" > + <option value="slack">Slack</option> <option value="google_sheets">Google Sheets</option>📝 Committable suggestion