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