From 91684207cefcae507c70284596d170f2d828a89b Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 28 Mar 2025 17:25:52 +0000 Subject: [PATCH 1/3] feat: add open in vscode button for devcontainers --- .../AgentDevcontainerCard.stories.tsx | 3 +- .../resources/AgentDevcontainerCard.tsx | 28 ++- site/src/modules/resources/AgentRow.tsx | 2 +- .../VSCodeDevContainerButton.tsx | 206 ++++++++++++++++++ 4 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx diff --git a/site/src/modules/resources/AgentDevcontainerCard.stories.tsx b/site/src/modules/resources/AgentDevcontainerCard.stories.tsx index 8e83168978ee5..e965efea75b6d 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.stories.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { MockWorkspace, + MockWorkspaceAgent, MockWorkspaceAgentContainer, MockWorkspaceAgentContainerPorts, } from "testHelpers/entities"; @@ -13,7 +14,7 @@ const meta: Meta = { container: MockWorkspaceAgentContainer, workspace: MockWorkspace, wildcardHostname: "*.wildcard.hostname", - agentName: "dev", + agent: MockWorkspaceAgent, }, }; diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index 70c91c5178bf2..a6c35e3456563 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -1,26 +1,35 @@ import Link from "@mui/material/Link"; import Tooltip, { type TooltipProps } from "@mui/material/Tooltip"; -import type { Workspace, WorkspaceAgentContainer } from "api/typesGenerated"; +import type { + Workspace, + WorkspaceAgent, + WorkspaceAgentContainer, +} from "api/typesGenerated"; import { ExternalLinkIcon } from "lucide-react"; import type { FC } from "react"; import { portForwardURL } from "utils/portForward"; import { AgentButton } from "./AgentButton"; import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton"; import { TerminalLink } from "./TerminalLink/TerminalLink"; +import { VSCodeDevContainerButton } from "./VSCodeDevContainerButton/VSCodeDevContainerButton"; type AgentDevcontainerCardProps = { + agent: WorkspaceAgent; container: WorkspaceAgentContainer; workspace: Workspace; wildcardHostname: string; - agentName: string; }; export const AgentDevcontainerCard: FC = ({ + agent, container, workspace, - agentName, wildcardHostname, }) => { + const folderPath = container.labels["devcontainer.local_folder"]; + const configFile = container.labels["devcontainer.config_file"]; + const containerFolder = container.volumes[folderPath]; + return (
= ({

Forwarded ports

+ + @@ -58,7 +76,7 @@ export const AgentDevcontainerCard: FC = ({ ? portForwardURL( wildcardHostname, port.host_port!, - agentName, + agent.name, workspace.name, workspace.owner_name, location.protocol === "https" ? "https" : "http", diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index ec45a8eec7c0a..c7de9d948ac41 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -290,7 +290,7 @@ export const AgentRow: FC = ({ container={container} workspace={workspace} wildcardHostname={proxy.preferredWildcardHostname} - agentName={agent.name} + agent={agent} /> ); })} diff --git a/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx new file mode 100644 index 0000000000000..3df3def63fd97 --- /dev/null +++ b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx @@ -0,0 +1,206 @@ +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { API } from "api/api"; +import type { DisplayApp } from "api/typesGenerated"; +import { VSCodeIcon } from "components/Icons/VSCodeIcon"; +import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon"; +import { type FC, useRef, useState } from "react"; +import { AgentButton } from "../AgentButton"; +import { DisplayAppNameMap } from "../AppLink/AppLink"; + +export interface VSCodeDevContainerButtonProps { + userName: string; + workspaceName: string; + agentName?: string; + folderPath: string; + devContainerPath: string; + devContainerFolder: string; + displayApps: readonly DisplayApp[]; +} + +type VSCodeVariant = "vscode" | "vscode-insiders"; + +const VARIANT_KEY = "vscode-variant"; + +export const VSCodeDevContainerButton: FC = ( + props, +) => { + const [isVariantMenuOpen, setIsVariantMenuOpen] = useState(false); + const previousVariant = localStorage.getItem(VARIANT_KEY); + const [variant, setVariant] = useState(() => { + if (!previousVariant) { + return "vscode"; + } + return previousVariant as VSCodeVariant; + }); + const menuAnchorRef = useRef(null); + + const selectVariant = (variant: VSCodeVariant) => { + localStorage.setItem(VARIANT_KEY, variant); + setVariant(variant); + setIsVariantMenuOpen(false); + }; + + const includesVSCodeDesktop = props.displayApps.includes("vscode"); + const includesVSCodeInsiders = props.displayApps.includes("vscode_insiders"); + + return includesVSCodeDesktop && includesVSCodeInsiders ? ( +
+ + {variant === "vscode" ? ( + + ) : ( + + )} + + { + setIsVariantMenuOpen(true); + }} + css={{ paddingLeft: 0, paddingRight: 0 }} + > + + + + + setIsVariantMenuOpen(false)} + css={{ + "& .MuiMenu-paper": { + width: menuAnchorRef.current?.clientWidth, + }, + }} + > + { + selectVariant("vscode"); + }} + > + + {DisplayAppNameMap.vscode} + + { + selectVariant("vscode-insiders"); + }} + > + + {DisplayAppNameMap.vscode_insiders} + + +
+ ) : includesVSCodeDesktop ? ( + + ) : ( + + ); +}; + +const VSCodeButton: FC = ({ + userName, + workspaceName, + agentName, + folderPath, + devContainerPath, + devContainerFolder, +}) => { + const [loading, setLoading] = useState(false); + + return ( + } + disabled={loading} + onClick={() => { + setLoading(true); + API.getApiKey() + .then(({ key }) => { + const query = new URLSearchParams({ + owner: userName, + workspace: workspaceName, + url: location.origin, + token: key, + devContainerPath, + devContainerFolder, + }); + if (agentName) { + query.set("agent", agentName); + } + if (folderPath) { + query.set("folder", folderPath); + } + + location.href = `vscode://coder.coder-remote/openDevContainer?${query.toString()}`; + }) + .catch((ex) => { + console.error(ex); + }) + .finally(() => { + setLoading(false); + }); + }} + > + {DisplayAppNameMap.vscode} + + ); +}; + +const VSCodeInsidersButton: FC = ({ + userName, + workspaceName, + agentName, + folderPath, + devContainerPath, + devContainerFolder, +}) => { + const [loading, setLoading] = useState(false); + + return ( + } + disabled={loading} + onClick={() => { + setLoading(true); + API.getApiKey() + .then(({ key }) => { + const query = new URLSearchParams({ + owner: userName, + workspace: workspaceName, + url: location.origin, + token: key, + devContainerPath, + devContainerFolder, + }); + if (agentName) { + query.set("agent", agentName); + } + if (folderPath) { + query.set("folder", folderPath); + } + + location.href = `vscode-insiders://coder.coder-remote/openDevContainer?${query.toString()}`; + }) + .catch((ex) => { + console.error(ex); + }) + .finally(() => { + setLoading(false); + }); + }} + > + {DisplayAppNameMap.vscode_insiders} + + ); +}; From 22c7743442f9df1339572fade8e50d76a14edf74 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Sat, 29 Mar 2025 01:42:37 +0000 Subject: [PATCH 2/3] chore: update query params sent to vscode extension --- .../resources/AgentDevcontainerCard.tsx | 4 +--- .../VSCodeDevContainerButton.tsx | 19 +++++-------------- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index a6c35e3456563..c668b380e1dde 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -27,7 +27,6 @@ export const AgentDevcontainerCard: FC = ({ wildcardHostname, }) => { const folderPath = container.labels["devcontainer.local_folder"]; - const configFile = container.labels["devcontainer.config_file"]; const containerFolder = container.volumes[folderPath]; return ( @@ -52,8 +51,7 @@ export const AgentDevcontainerCard: FC = ({ diff --git a/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx index 3df3def63fd97..3b32c672e8e8f 100644 --- a/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx +++ b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx @@ -14,8 +14,7 @@ export interface VSCodeDevContainerButtonProps { userName: string; workspaceName: string; agentName?: string; - folderPath: string; - devContainerPath: string; + devContainerName: string; devContainerFolder: string; displayApps: readonly DisplayApp[]; } @@ -113,8 +112,7 @@ const VSCodeButton: FC = ({ userName, workspaceName, agentName, - folderPath, - devContainerPath, + devContainerName, devContainerFolder, }) => { const [loading, setLoading] = useState(false); @@ -132,15 +130,12 @@ const VSCodeButton: FC = ({ workspace: workspaceName, url: location.origin, token: key, - devContainerPath, + devContainerName, devContainerFolder, }); if (agentName) { query.set("agent", agentName); } - if (folderPath) { - query.set("folder", folderPath); - } location.href = `vscode://coder.coder-remote/openDevContainer?${query.toString()}`; }) @@ -161,8 +156,7 @@ const VSCodeInsidersButton: FC = ({ userName, workspaceName, agentName, - folderPath, - devContainerPath, + devContainerName, devContainerFolder, }) => { const [loading, setLoading] = useState(false); @@ -180,15 +174,12 @@ const VSCodeInsidersButton: FC = ({ workspace: workspaceName, url: location.origin, token: key, - devContainerPath, + devContainerName, devContainerFolder, }); if (agentName) { query.set("agent", agentName); } - if (folderPath) { - query.set("folder", folderPath); - } location.href = `vscode-insiders://coder.coder-remote/openDevContainer?${query.toString()}`; }) From caee9c0c073201d7113579e8cbbd640c7deb60f5 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 1 Apr 2025 10:32:17 +0100 Subject: [PATCH 3/3] chore: add tests --- .../VSCodeDevContainerButton.stories.tsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.stories.tsx diff --git a/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.stories.tsx b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.stories.tsx new file mode 100644 index 0000000000000..a16eb58ba72b3 --- /dev/null +++ b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.stories.tsx @@ -0,0 +1,60 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; +import { VSCodeDevContainerButton } from "./VSCodeDevContainerButton"; + +const meta: Meta = { + title: "modules/resources/VSCodeDevContainerButton", + component: VSCodeDevContainerButton, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + userName: MockWorkspace.owner_name, + workspaceName: MockWorkspace.name, + agentName: MockWorkspaceAgent.name, + devContainerName: "musing_ride", + devContainerFolder: "/workspace/coder", + displayApps: [ + "vscode", + "vscode_insiders", + "port_forwarding_helper", + "ssh_helper", + "web_terminal", + ], + }, +}; + +export const VSCodeOnly: Story = { + args: { + userName: MockWorkspace.owner_name, + workspaceName: MockWorkspace.name, + agentName: MockWorkspaceAgent.name, + devContainerName: "nifty_borg", + devContainerFolder: "/workspace/coder", + displayApps: [ + "vscode", + "port_forwarding_helper", + "ssh_helper", + "web_terminal", + ], + }, +}; + +export const InsidersOnly: Story = { + args: { + userName: MockWorkspace.owner_name, + workspaceName: MockWorkspace.name, + agentName: MockWorkspaceAgent.name, + devContainerName: "amazing_swartz", + devContainerFolder: "/workspace/coder", + displayApps: [ + "vscode_insiders", + "port_forwarding_helper", + "ssh_helper", + "web_terminal", + ], + }, +}; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy