From db3078fcf81b173460ab23e46aaa1eba87777e9f Mon Sep 17 00:00:00 2001 From: Danilo Woznica Date: Wed, 15 May 2024 08:49:01 +0000 Subject: [PATCH 1/3] feat(export): support private sandboxes in workspace --- .../UnstyledOpenInCodeSandboxButton.tsx | 67 ++++++++++++++++--- .../src/contexts/sandpackContext.tsx | 2 + .../src/contexts/utils/useAppState.ts | 2 - sandpack-react/src/types.ts | 19 ++++++ 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx b/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx index 62c2a50d0..2c25d037a 100644 --- a/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx +++ b/sandpack-react/src/components/common/OpenInCodeSandboxButton/UnstyledOpenInCodeSandboxButton.tsx @@ -4,7 +4,7 @@ import LZString from "lz-string"; import * as React from "react"; import { useSandpack } from "../../../hooks/useSandpack"; -import type { SandboxEnvironment } from "../../../types"; +import type { SandboxEnvironment, SandpackState } from "../../../types"; // eslint-disable-next-line @typescript-eslint/no-explicit-any const getParameters = (parameters: Record): string => @@ -47,19 +47,70 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< React.HtmlHTMLAttributes > = ({ children, ...props }) => { const { sandpack } = useSandpack(); - const formRef = React.useRef(null); + if (sandpack.exportOptions) { + return ; + } + + return ; +}; + +export const ExportToWorkspaceButton: React.FC< + React.HtmlHTMLAttributes & { state: SandpackState } +> = ({ children, state, ...props }) => { + const submit = async () => { + if (!state.exportOptions?.apiToken) { + throw new Error("Missing `apiToken` property"); + } + + const response = await fetch("https://api.codesandbox.io/sandbox", { + method: "POST", + body: JSON.stringify({ + files: state.files, + privacy: state.exportOptions.privacy === "public" ? 0 : 2, + }), + headers: { + Authorization: `Bearer ${state.exportOptions.apiToken}`, + "Content-Type": "application/json", + "X-CSB-API-Version": "2023-07-01", + }, + }); + + const data: { data: { alias: string } } = await response.json(); + + window.open( + `https://codesandbox.io/p/sandbox/${data.data.alias}?file=/src/App.js&utm-source=storybook-addon`, + "_blank" + ); + }; + + return ( + + ); +}; + +const RegularExportButton: React.FC< + React.HtmlHTMLAttributes & { state: SandpackState } +> = ({ children, state, ...props }) => { + const formRef = React.useRef(null); const [paramsValues, setParamsValues] = React.useState(); React.useEffect( function debounce() { const timer = setTimeout(() => { - const params = getFileParameters(sandpack.files, sandpack.environment); + const params = getFileParameters(state.files, state.environment); const searchParams = new URLSearchParams({ parameters: params, query: new URLSearchParams({ - file: sandpack.activeFile, + file: state.activeFile, utm_medium: "sandpack", }).toString(), }); @@ -71,7 +122,7 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< clearTimeout(timer); }; }, - [sandpack.activeFile, sandpack.environment, sandpack.files] + [state.activeFile, state.environment, state.files] ); /** @@ -96,9 +147,7 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< {Array.from( paramsValues as unknown as Array<[string, string]>, @@ -115,7 +164,7 @@ export const UnstyledOpenInCodeSandboxButton: React.FC< return ( = (props) => { ...clientOperations, autoReload: props.options?.autoReload ?? true, + teamId: props?.teamId, + exportOptions: props?.customSetup?.exportOptions, listen: addListener, dispatch: dispatchMessage, diff --git a/sandpack-react/src/contexts/utils/useAppState.ts b/sandpack-react/src/contexts/utils/useAppState.ts index 2f919a824..edba3dec8 100644 --- a/sandpack-react/src/contexts/utils/useAppState.ts +++ b/sandpack-react/src/contexts/utils/useAppState.ts @@ -7,7 +7,6 @@ import { getSandpackStateFromProps } from "../../utils/sandpackUtils"; interface SandpackAppState { editorState: "pristine" | "dirty"; - teamId?: string; } type UseAppState = ( @@ -18,7 +17,6 @@ type UseAppState = ( export const useAppState: UseAppState = (props, files) => { const [state, setState] = useState({ editorState: "pristine", - teamId: props.teamId, }); const originalStateFromProps = getSandpackStateFromProps(props); diff --git a/sandpack-react/src/types.ts b/sandpack-react/src/types.ts index f3448a7fd..c45a4b57a 100644 --- a/sandpack-react/src/types.ts +++ b/sandpack-react/src/types.ts @@ -241,6 +241,24 @@ export interface SandpackSetup { * ``` */ npmRegistries?: NpmRegistry[]; + + exportOptions?: SandpackExportOptions; +} + +interface SandpackExportOptions { + /** + * Workspace API key from codesandbox.io/t/permissions. + * When set, the sandbox will be create inside the given workspace id. + */ + apiToken: string; + + /** + * The default visibility of the new sandboxes inside the workspace. + * + * @note Use `private` if there is a private registry or private NPM + * configured in your workspace. + */ + privacy: "private" | "public"; } /** @@ -561,6 +579,7 @@ export interface SandpackState { */ editorState: EditorState; teamId?: string; + exportOptions?: SandpackExportOptions; error: SandpackError | null; files: SandpackBundlerFiles; environment?: SandboxEnvironment; From f08901893214dd4c9c193e047d8dbe45a4295607 Mon Sep 17 00:00:00 2001 From: Danilo Woznica Date: Mon, 27 May 2024 13:50:32 +0000 Subject: [PATCH 2/3] docs --- .../getting-started/private-packages.mdx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/website/docs/src/pages/getting-started/private-packages.mdx b/website/docs/src/pages/getting-started/private-packages.mdx index 2e3d95a62..58236b735 100644 --- a/website/docs/src/pages/getting-started/private-packages.mdx +++ b/website/docs/src/pages/getting-started/private-packages.mdx @@ -60,6 +60,29 @@ export default function App() { + +## Exporting sandboxes + +Once Sandpack is configured, you can use the "Open in CodeSandbox" button to export sandboxes to CodeSandbox effortlessly. However in order to seamless export the sandbox, you need to provide a CodeSandbox API key that will be used to create a sandbox inside your workspace. + +1. Generate a API token in your dashboard: [codesandbox.io/t/permissions](https://codesandbox.io/t/permissions.). +2. Use the generate token to setup your Sandpack instance: + +```jsx +", + privacy: "private", // "public" | "private" + } + }} +/> +``` + + + This sandbox will be created inside the given workspace and can be shared with team members. + + ## Security It is important to us to ensure that the information and tokens of the npm registry are kept private. As such, we have added some extra measures to prevent any type of leakage. From 37a8d9a9182aeb8101eb7bc5db5ee3aca0d9178b Mon Sep 17 00:00:00 2001 From: Danilo Woznica Date: Wed, 29 May 2024 09:59:40 +0100 Subject: [PATCH 3/3] Update website/docs/src/pages/getting-started/private-packages.mdx Co-authored-by: Ives van Hoorne --- website/docs/src/pages/getting-started/private-packages.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/src/pages/getting-started/private-packages.mdx b/website/docs/src/pages/getting-started/private-packages.mdx index 58236b735..72259cf8b 100644 --- a/website/docs/src/pages/getting-started/private-packages.mdx +++ b/website/docs/src/pages/getting-started/private-packages.mdx @@ -65,7 +65,7 @@ export default function App() { Once Sandpack is configured, you can use the "Open in CodeSandbox" button to export sandboxes to CodeSandbox effortlessly. However in order to seamless export the sandbox, you need to provide a CodeSandbox API key that will be used to create a sandbox inside your workspace. -1. Generate a API token in your dashboard: [codesandbox.io/t/permissions](https://codesandbox.io/t/permissions.). +1. Generate a API token in your dashboard: [codesandbox.io/t/permissions](https://codesandbox.io/t/permissions). 2. Use the generate token to setup your Sandpack instance: ```jsx