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..c668b380e1dde 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -1,26 +1,34 @@ 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 containerFolder = container.volumes[folderPath]; + return (
= ({

Forwarded ports

+ + @@ -58,7 +74,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.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", + ], + }, +}; diff --git a/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx new file mode 100644 index 0000000000000..3b32c672e8e8f --- /dev/null +++ b/site/src/modules/resources/VSCodeDevContainerButton/VSCodeDevContainerButton.tsx @@ -0,0 +1,197 @@ +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; + devContainerName: 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, + devContainerName, + 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, + devContainerName, + devContainerFolder, + }); + if (agentName) { + query.set("agent", agentName); + } + + 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, + devContainerName, + 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, + devContainerName, + devContainerFolder, + }); + if (agentName) { + query.set("agent", agentName); + } + + location.href = `vscode-insiders://coder.coder-remote/openDevContainer?${query.toString()}`; + }) + .catch((ex) => { + console.error(ex); + }) + .finally(() => { + setLoading(false); + }); + }} + > + {DisplayAppNameMap.vscode_insiders} + + ); +};