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} + + ); +}; 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