diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a1aeeca8a9..ede6f90a01 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2374,6 +2374,18 @@ class ApiMethods { ); } }; + + getAgentContainers = async (agentId: string, labels?: string[]) => { + const params = new URLSearchParams( + labels?.map((label) => ["label", label]), + ); + + const res = + await this.axios.get( + `/api/v2/workspaceagents/${agentId}/containers?${params.toString()}`, + ); + return res.data; + }; } // This is a hard coded CSRF token/cookie pair for local development. In prod, diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx new file mode 100644 index 0000000000..fc58c21f95 --- /dev/null +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -0,0 +1,74 @@ +import Link from "@mui/material/Link"; +import type { Workspace, WorkspaceAgentDevcontainer } 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"; + +type AgentDevcontainerCardProps = { + container: WorkspaceAgentDevcontainer; + workspace: Workspace; + wildcardHostname: string; + agentName: string; +}; + +export const AgentDevcontainerCard: FC = ({ + container, + workspace, + agentName, + wildcardHostname, +}) => { + return ( +
+
+

+ {container.name} +

+ + +
+ +

Forwarded ports

+ +
+ + {wildcardHostname !== "" && + container.ports.map((port) => { + return ( + } + href={portForwardURL( + wildcardHostname, + port.port, + agentName, + workspace.name, + workspace.owner_name, + location.protocol === "https" ? "https" : "http", + )} + > + {port.process_name || + `${port.port}/${port.network.toUpperCase()}`} + + ); + })} +
+
+ ); +}; diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index 9e5caed677..1b9761f28e 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -3,6 +3,7 @@ import Button from "@mui/material/Button"; import Collapse from "@mui/material/Collapse"; import Divider from "@mui/material/Divider"; import Skeleton from "@mui/material/Skeleton"; +import { API } from "api/api"; import { xrayScan } from "api/queries/integrations"; import type { Template, @@ -25,6 +26,7 @@ import { import { useQuery } from "react-query"; import AutoSizer from "react-virtualized-auto-sizer"; import type { FixedSizeList as List, ListOnScrollProps } from "react-window"; +import { AgentDevcontainerCard } from "./AgentDevcontainerCard"; import { AgentLatency } from "./AgentLatency"; import { AGENT_LOG_LINE_HEIGHT } from "./AgentLogs/AgentLogLine"; import { AgentLogs } from "./AgentLogs/AgentLogs"; @@ -35,7 +37,7 @@ import { AgentVersion } from "./AgentVersion"; import { AppLink } from "./AppLink/AppLink"; import { DownloadAgentLogsButton } from "./DownloadAgentLogsButton"; import { PortForwardButton } from "./PortForwardButton"; -import { SSHButton } from "./SSHButton/SSHButton"; +import { AgentSSHButton } from "./SSHButton/SSHButton"; import { TerminalLink } from "./TerminalLink/TerminalLink"; import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton"; import { XRayScanAlert } from "./XRayScanAlert"; @@ -152,6 +154,18 @@ export const AgentRow: FC = ({ setBottomOfLogs(distanceFromBottom < AGENT_LOG_LINE_HEIGHT); }, []); + const { data: containers } = useQuery({ + queryKey: ["agents", agent.id, "containers"], + queryFn: () => + // Only return devcontainers + API.getAgentContainers(agent.id, [ + "devcontainer.config_file=", + "devcontainer.local_folder=", + ]), + enabled: agent.status === "connected", + select: (res) => res.containers.filter((c) => c.status === "running"), + }); + return ( = ({ {showBuiltinApps && (
{!hideSSHButton && agent.display_apps.includes("ssh_helper") && ( - )} - {proxy.preferredWildcardHostname && - proxy.preferredWildcardHostname !== "" && + {proxy.preferredWildcardHostname !== "" && agent.display_apps.includes("port_forwarding_helper") && ( = ({ )} + {containers && containers.length > 0 && ( +
+ {containers.map((container) => { + return ( + + ); + })} +
+ )} + = { - title: "modules/resources/SSHButton", - component: SSHButton, +const meta: Meta = { + title: "modules/resources/AgentSSHButton", + component: AgentSSHButton, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Closed: Story = { args: { diff --git a/site/src/modules/resources/SSHButton/SSHButton.tsx b/site/src/modules/resources/SSHButton/SSHButton.tsx index 3d94b33375..d5351a3ff5 100644 --- a/site/src/modules/resources/SSHButton/SSHButton.tsx +++ b/site/src/modules/resources/SSHButton/SSHButton.tsx @@ -17,13 +17,13 @@ import { type ClassName, useClassName } from "hooks/useClassName"; import type { FC } from "react"; import { docs } from "utils/docs"; -export interface SSHButtonProps { +export interface AgentSSHButtonProps { workspaceName: string; agentName: string; sshPrefix?: string; } -export const SSHButton: FC = ({ +export const AgentSSHButton: FC = ({ workspaceName, agentName, sshPrefix, @@ -82,6 +82,56 @@ export const SSHButton: FC = ({ ); }; +export interface AgentDevcontainerSSHButtonProps { + workspace: string; + container: string; +} + +export const AgentDevcontainerSSHButton: FC< + AgentDevcontainerSSHButtonProps +> = ({ workspace, container }) => { + const paper = useClassName(classNames.paper, []); + + return ( + + + + + + + + Run the following commands to connect with SSH: + + +
    + + + +
+ + + + Install Coder CLI + + + SSH configuration + + +
+
+ ); +}; + interface SSHStepProps { helpText: string; codeExample: string; diff --git a/site/src/modules/resources/TerminalLink/TerminalLink.tsx b/site/src/modules/resources/TerminalLink/TerminalLink.tsx index 4d709dc482..f7a07131e4 100644 --- a/site/src/modules/resources/TerminalLink/TerminalLink.tsx +++ b/site/src/modules/resources/TerminalLink/TerminalLink.tsx @@ -11,9 +11,10 @@ export const Language = { }; export interface TerminalLinkProps { - agentName?: TypesGen.WorkspaceAgent["name"]; - userName?: TypesGen.User["username"]; - workspaceName: TypesGen.Workspace["name"]; + workspaceName: string; + agentName?: string; + userName?: string; + containerName?: string; } /** @@ -27,11 +28,16 @@ export const TerminalLink: FC = ({ agentName, userName = "me", workspaceName, + containerName, }) => { + const params = new URLSearchParams(); + if (containerName) { + params.append("container", containerName); + } // Always use the primary for the terminal link. This is a relative link. const href = `/@${userName}/${workspaceName}${ agentName ? `.${agentName}` : "" - }/terminal`; + }/terminal?${params.toString()}`; return (