From df1cc9509164e8df6d3f16f506a295ae855a9b79 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 3 Mar 2025 19:30:27 +0000 Subject: [PATCH 1/8] feat: display devcontainer in the UI --- site/src/api/api.ts | 8 +++ .../resources/AgentDevcontainerCard.tsx | 51 ++++++++++++++++ site/src/modules/resources/AgentRow.tsx | 27 ++++++++- .../resources/SSHButton/SSHButton.stories.tsx | 10 ++-- .../modules/resources/SSHButton/SSHButton.tsx | 58 ++++++++++++++++++- 5 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 site/src/modules/resources/AgentDevcontainerCard.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a1aeeca8a9e59..b61d0f988b341 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2374,6 +2374,14 @@ class ApiMethods { ); } }; + + getAgentContainers = async (agentId: string) => { + const res = + await this.axios.get( + `/api/v2/workspaceagents/${agentId}/containers`, + ); + 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 0000000000000..ecbaebf169e67 --- /dev/null +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -0,0 +1,51 @@ +import Link from "@mui/material/Link"; +import type { WorkspaceAgentDevcontainer } from "api/typesGenerated"; +import type { FC } from "react"; +import { AgentButton } from "./AgentButton"; +import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton"; + +type AgentDevcontainerCardProps = { + container: WorkspaceAgentDevcontainer; + workspace: string; +}; + +export const AgentDevcontainerCard: FC = ({ + container, + workspace, +}) => { + return ( +
+
+

+ {container.name} +

+ + +
+ +

Forwarded ports

+ +
+ {container.ports.map((port) => { + return ( + + {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 9e5caed677ee1..2905f40b24bda 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,13 @@ export const AgentRow: FC = ({ setBottomOfLogs(distanceFromBottom < AGENT_LOG_LINE_HEIGHT); }, []); + const { data: containers } = useQuery({ + queryKey: ["agents", agent.id, "containers"], + queryFn: () => API.getAgentContainers(agent.id), + enabled: agent.status === "connected", + select: (res) => res.containers, + }); + return ( = ({ {showBuiltinApps && (
{!hideSSHButton && agent.display_apps.includes("ssh_helper") && ( - = ({ )} + {containers && ( +
+ {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 3d94b33375c0b..3328534d6a2fd 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,60 @@ 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; From 3da58742634acc1ca39da9ddbc0b999d7a75a87d Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 3 Mar 2025 19:39:49 +0000 Subject: [PATCH 2/8] Make UI prettuer --- site/src/modules/resources/AgentDevcontainerCard.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index ecbaebf169e67..5aacbdb3fe69f 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -3,6 +3,8 @@ import type { WorkspaceAgentDevcontainer } from "api/typesGenerated"; import type { FC } from "react"; import { AgentButton } from "./AgentButton"; import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton"; +import { ExternalLinkIcon } from "lucide-react"; +import { TerminalLink } from "./TerminalLink/TerminalLink"; type AgentDevcontainerCardProps = { container: WorkspaceAgentDevcontainer; @@ -32,6 +34,7 @@ export const AgentDevcontainerCard: FC = ({

Forwarded ports

+ {container.ports.map((port) => { return ( = ({ color="inherit" component={AgentButton} underline="none" + startIcon={} > {port.process_name || `${port.port}/${port.network.toUpperCase()}`} From 9ad346e7764092dbe78ddf8d143feede24ba0014 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Mon, 3 Mar 2025 19:40:04 +0000 Subject: [PATCH 3/8] Fmt --- site/src/modules/resources/AgentDevcontainerCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index 5aacbdb3fe69f..d9f941de39ee3 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -1,9 +1,9 @@ import Link from "@mui/material/Link"; import type { WorkspaceAgentDevcontainer } from "api/typesGenerated"; +import { ExternalLinkIcon } from "lucide-react"; import type { FC } from "react"; import { AgentButton } from "./AgentButton"; import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton"; -import { ExternalLinkIcon } from "lucide-react"; import { TerminalLink } from "./TerminalLink/TerminalLink"; type AgentDevcontainerCardProps = { From 8b9119a0a89e6e5193828e99ef45aae20bd88899 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 4 Mar 2025 13:52:51 +0000 Subject: [PATCH 4/8] fix terminal and devcontainer links --- .../resources/AgentDevcontainerCard.tsx | 26 ++++++++++++++++--- site/src/modules/resources/AgentRow.tsx | 4 ++- .../resources/TerminalLink/TerminalLink.tsx | 14 +++++++--- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index d9f941de39ee3..642815e9f7685 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -1,19 +1,24 @@ import Link from "@mui/material/Link"; -import type { WorkspaceAgentDevcontainer } from "api/typesGenerated"; +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: string; + workspace: Workspace; + host: string; + agentName: string; }; export const AgentDevcontainerCard: FC = ({ container, workspace, + agentName, + host, }) => { return (
= ({ @@ -34,7 +39,12 @@ export const AgentDevcontainerCard: FC = ({

Forwarded ports

- + {container.ports.map((port) => { return ( = ({ component={AgentButton} underline="none" startIcon={} + href={portForwardURL( + host, + 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 2905f40b24bda..8d934f5a802b9 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -283,7 +283,9 @@ export const AgentRow: FC = ({ ); })} diff --git a/site/src/modules/resources/TerminalLink/TerminalLink.tsx b/site/src/modules/resources/TerminalLink/TerminalLink.tsx index 4d709dc482e70..f7a07131e4cd0 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 ( Date: Tue, 4 Mar 2025 10:58:52 -0300 Subject: [PATCH 5/8] Apply suggestions from code review Co-authored-by: Cian Johnston --- site/src/modules/resources/SSHButton/SSHButton.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/site/src/modules/resources/SSHButton/SSHButton.tsx b/site/src/modules/resources/SSHButton/SSHButton.tsx index 3328534d6a2fd..d5351a3ff5466 100644 --- a/site/src/modules/resources/SSHButton/SSHButton.tsx +++ b/site/src/modules/resources/SSHButton/SSHButton.tsx @@ -113,12 +113,8 @@ export const AgentDevcontainerSSHButton: FC<
    -
From 6edc3d0f7f0b7d9a0acecb6ccfaa28f3ee713698 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 4 Mar 2025 14:52:36 +0000 Subject: [PATCH 6/8] Apply Cian's suggestions --- site/src/api/api.ts | 8 ++++++-- site/src/modules/resources/AgentRow.tsx | 11 ++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index b61d0f988b341..ede6f90a0133b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2375,10 +2375,14 @@ class ApiMethods { } }; - getAgentContainers = async (agentId: string) => { + 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`, + `/api/v2/workspaceagents/${agentId}/containers?${params.toString()}`, ); return res.data; }; diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index 8d934f5a802b9..c770d1b265733 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -156,7 +156,12 @@ export const AgentRow: FC = ({ const { data: containers } = useQuery({ queryKey: ["agents", agent.id, "containers"], - queryFn: () => API.getAgentContainers(agent.id), + queryFn: () => + // Only return devcontainers + API.getAgentContainers(agent.id, [ + "devcontainer.config_file=", + "devcontainer.local_folder=", + ]), enabled: agent.status === "connected", select: (res) => res.containers, }); @@ -210,7 +215,7 @@ export const AgentRow: FC = ({ proxy.preferredWildcardHostname !== "" && agent.display_apps.includes("port_forwarding_helper") && ( = ({ key={container.id} container={container} workspace={workspace} - host={proxy.preferredWildcardHostname} + host={proxy.preferredWildcardHostname || window.location.host} agentName={agent.name} /> ); From b6a36e37767b74f2e674d57ff4a49ebbb6a38459 Mon Sep 17 00:00:00 2001 From: BrunoQuaresma Date: Tue, 4 Mar 2025 17:00:16 +0000 Subject: [PATCH 7/8] Only show running containers --- site/src/modules/resources/AgentRow.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index c770d1b265733..e5b0a320c3ce7 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -163,7 +163,7 @@ export const AgentRow: FC = ({ "devcontainer.local_folder=", ]), enabled: agent.status === "connected", - select: (res) => res.containers, + select: (res) => res.containers.filter((c) => c.status === "running"), }); return ( @@ -215,7 +215,7 @@ export const AgentRow: FC = ({ proxy.preferredWildcardHostname !== "" && agent.display_apps.includes("port_forwarding_helper") && ( = ({
)} - {containers && ( -
+ {containers && containers.length > 0 && ( +
{containers.map((container) => { return ( Date: Tue, 4 Mar 2025 17:19:32 +0000 Subject: [PATCH 8/8] Portforward only can be show if wildcardHostname is set --- .../resources/AgentDevcontainerCard.tsx | 49 ++++++++++--------- site/src/modules/resources/AgentRow.tsx | 5 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/site/src/modules/resources/AgentDevcontainerCard.tsx b/site/src/modules/resources/AgentDevcontainerCard.tsx index 642815e9f7685..fc58c21f95bcb 100644 --- a/site/src/modules/resources/AgentDevcontainerCard.tsx +++ b/site/src/modules/resources/AgentDevcontainerCard.tsx @@ -10,7 +10,7 @@ import { TerminalLink } from "./TerminalLink/TerminalLink"; type AgentDevcontainerCardProps = { container: WorkspaceAgentDevcontainer; workspace: Workspace; - host: string; + wildcardHostname: string; agentName: string; }; @@ -18,7 +18,7 @@ export const AgentDevcontainerCard: FC = ({ container, workspace, agentName, - host, + wildcardHostname, }) => { return (
= ({ containerName={container.name} userName={workspace.owner_name} /> - {container.ports.map((port) => { - return ( - } - href={portForwardURL( - host, - port.port, - agentName, - workspace.name, - workspace.owner_name, - location.protocol === "https" ? "https" : "http", - )} - > - {port.process_name || - `${port.port}/${port.network.toUpperCase()}`} - - ); - })} + {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 e5b0a320c3ce7..1b9761f28ea40 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -211,8 +211,7 @@ export const AgentRow: FC = ({ sshPrefix={sshPrefix} /> )} - {proxy.preferredWildcardHostname && - proxy.preferredWildcardHostname !== "" && + {proxy.preferredWildcardHostname !== "" && agent.display_apps.includes("port_forwarding_helper") && ( = ({ key={container.id} container={container} workspace={workspace} - host={proxy.preferredWildcardHostname || window.location.host} + wildcardHostname={proxy.preferredWildcardHostname} agentName={agent.name} /> );