Skip to content

Commit 861c4b1

Browse files
feat: add devcontainer in the UI (#16800)
![image](https://github.com/user-attachments/assets/361f9e69-dec8-47c8-b075-7c13ce84c7e8) Related to #16422 --------- Co-authored-by: Cian Johnston <cian@coder.com>
1 parent 73057eb commit 861c4b1

File tree

6 files changed

+186
-15
lines changed

6 files changed

+186
-15
lines changed

site/src/api/api.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2374,6 +2374,18 @@ class ApiMethods {
23742374
);
23752375
}
23762376
};
2377+
2378+
getAgentContainers = async (agentId: string, labels?: string[]) => {
2379+
const params = new URLSearchParams(
2380+
labels?.map((label) => ["label", label]),
2381+
);
2382+
2383+
const res =
2384+
await this.axios.get<TypesGen.WorkspaceAgentListContainersResponse>(
2385+
`/api/v2/workspaceagents/${agentId}/containers?${params.toString()}`,
2386+
);
2387+
return res.data;
2388+
};
23772389
}
23782390

23792391
// This is a hard coded CSRF token/cookie pair for local development. In prod,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import Link from "@mui/material/Link";
2+
import type { Workspace, WorkspaceAgentDevcontainer } from "api/typesGenerated";
3+
import { ExternalLinkIcon } from "lucide-react";
4+
import type { FC } from "react";
5+
import { portForwardURL } from "utils/portForward";
6+
import { AgentButton } from "./AgentButton";
7+
import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton";
8+
import { TerminalLink } from "./TerminalLink/TerminalLink";
9+
10+
type AgentDevcontainerCardProps = {
11+
container: WorkspaceAgentDevcontainer;
12+
workspace: Workspace;
13+
wildcardHostname: string;
14+
agentName: string;
15+
};
16+
17+
export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
18+
container,
19+
workspace,
20+
agentName,
21+
wildcardHostname,
22+
}) => {
23+
return (
24+
<section
25+
className="border border-border border-dashed rounded p-6 "
26+
key={container.id}
27+
>
28+
<header className="flex justify-between">
29+
<h3 className="m-0 text-xs font-medium text-content-secondary">
30+
{container.name}
31+
</h3>
32+
33+
<AgentDevcontainerSSHButton
34+
workspace={workspace.name}
35+
container={container.name}
36+
/>
37+
</header>
38+
39+
<h4 className="m-0 text-xl font-semibold">Forwarded ports</h4>
40+
41+
<div className="flex gap-4 flex-wrap mt-4">
42+
<TerminalLink
43+
workspaceName={workspace.name}
44+
agentName={agentName}
45+
containerName={container.name}
46+
userName={workspace.owner_name}
47+
/>
48+
{wildcardHostname !== "" &&
49+
container.ports.map((port) => {
50+
return (
51+
<Link
52+
key={port.port}
53+
color="inherit"
54+
component={AgentButton}
55+
underline="none"
56+
startIcon={<ExternalLinkIcon className="size-icon-sm" />}
57+
href={portForwardURL(
58+
wildcardHostname,
59+
port.port,
60+
agentName,
61+
workspace.name,
62+
workspace.owner_name,
63+
location.protocol === "https" ? "https" : "http",
64+
)}
65+
>
66+
{port.process_name ||
67+
`${port.port}/${port.network.toUpperCase()}`}
68+
</Link>
69+
);
70+
})}
71+
</div>
72+
</section>
73+
);
74+
};

site/src/modules/resources/AgentRow.tsx

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Button from "@mui/material/Button";
33
import Collapse from "@mui/material/Collapse";
44
import Divider from "@mui/material/Divider";
55
import Skeleton from "@mui/material/Skeleton";
6+
import { API } from "api/api";
67
import { xrayScan } from "api/queries/integrations";
78
import type {
89
Template,
@@ -25,6 +26,7 @@ import {
2526
import { useQuery } from "react-query";
2627
import AutoSizer from "react-virtualized-auto-sizer";
2728
import type { FixedSizeList as List, ListOnScrollProps } from "react-window";
29+
import { AgentDevcontainerCard } from "./AgentDevcontainerCard";
2830
import { AgentLatency } from "./AgentLatency";
2931
import { AGENT_LOG_LINE_HEIGHT } from "./AgentLogs/AgentLogLine";
3032
import { AgentLogs } from "./AgentLogs/AgentLogs";
@@ -35,7 +37,7 @@ import { AgentVersion } from "./AgentVersion";
3537
import { AppLink } from "./AppLink/AppLink";
3638
import { DownloadAgentLogsButton } from "./DownloadAgentLogsButton";
3739
import { PortForwardButton } from "./PortForwardButton";
38-
import { SSHButton } from "./SSHButton/SSHButton";
40+
import { AgentSSHButton } from "./SSHButton/SSHButton";
3941
import { TerminalLink } from "./TerminalLink/TerminalLink";
4042
import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton";
4143
import { XRayScanAlert } from "./XRayScanAlert";
@@ -152,6 +154,18 @@ export const AgentRow: FC<AgentRowProps> = ({
152154
setBottomOfLogs(distanceFromBottom < AGENT_LOG_LINE_HEIGHT);
153155
}, []);
154156

157+
const { data: containers } = useQuery({
158+
queryKey: ["agents", agent.id, "containers"],
159+
queryFn: () =>
160+
// Only return devcontainers
161+
API.getAgentContainers(agent.id, [
162+
"devcontainer.config_file=",
163+
"devcontainer.local_folder=",
164+
]),
165+
enabled: agent.status === "connected",
166+
select: (res) => res.containers.filter((c) => c.status === "running"),
167+
});
168+
155169
return (
156170
<Stack
157171
key={agent.id}
@@ -191,14 +205,13 @@ export const AgentRow: FC<AgentRowProps> = ({
191205
{showBuiltinApps && (
192206
<div css={{ display: "flex" }}>
193207
{!hideSSHButton && agent.display_apps.includes("ssh_helper") && (
194-
<SSHButton
208+
<AgentSSHButton
195209
workspaceName={workspace.name}
196210
agentName={agent.name}
197211
sshPrefix={sshPrefix}
198212
/>
199213
)}
200-
{proxy.preferredWildcardHostname &&
201-
proxy.preferredWildcardHostname !== "" &&
214+
{proxy.preferredWildcardHostname !== "" &&
202215
agent.display_apps.includes("port_forwarding_helper") && (
203216
<PortForwardButton
204217
host={proxy.preferredWildcardHostname}
@@ -267,6 +280,22 @@ export const AgentRow: FC<AgentRowProps> = ({
267280
</section>
268281
)}
269282

283+
{containers && containers.length > 0 && (
284+
<section className="flex flex-col gap-4">
285+
{containers.map((container) => {
286+
return (
287+
<AgentDevcontainerCard
288+
key={container.id}
289+
container={container}
290+
workspace={workspace}
291+
wildcardHostname={proxy.preferredWildcardHostname}
292+
agentName={agent.name}
293+
/>
294+
);
295+
})}
296+
</section>
297+
)}
298+
270299
<AgentMetadata
271300
storybookMetadata={storybookAgentMetadata}
272301
agent={agent}

site/src/modules/resources/SSHButton/SSHButton.stories.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import type { Meta, StoryObj } from "@storybook/react";
22
import { userEvent, within } from "@storybook/test";
33
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
44
import { withDesktopViewport } from "testHelpers/storybook";
5-
import { SSHButton } from "./SSHButton";
5+
import { AgentSSHButton } from "./SSHButton";
66

7-
const meta: Meta<typeof SSHButton> = {
8-
title: "modules/resources/SSHButton",
9-
component: SSHButton,
7+
const meta: Meta<typeof AgentSSHButton> = {
8+
title: "modules/resources/AgentSSHButton",
9+
component: AgentSSHButton,
1010
};
1111

1212
export default meta;
13-
type Story = StoryObj<typeof SSHButton>;
13+
type Story = StoryObj<typeof AgentSSHButton>;
1414

1515
export const Closed: Story = {
1616
args: {

site/src/modules/resources/SSHButton/SSHButton.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ import { type ClassName, useClassName } from "hooks/useClassName";
1717
import type { FC } from "react";
1818
import { docs } from "utils/docs";
1919

20-
export interface SSHButtonProps {
20+
export interface AgentSSHButtonProps {
2121
workspaceName: string;
2222
agentName: string;
2323
sshPrefix?: string;
2424
}
2525

26-
export const SSHButton: FC<SSHButtonProps> = ({
26+
export const AgentSSHButton: FC<AgentSSHButtonProps> = ({
2727
workspaceName,
2828
agentName,
2929
sshPrefix,
@@ -82,6 +82,56 @@ export const SSHButton: FC<SSHButtonProps> = ({
8282
);
8383
};
8484

85+
export interface AgentDevcontainerSSHButtonProps {
86+
workspace: string;
87+
container: string;
88+
}
89+
90+
export const AgentDevcontainerSSHButton: FC<
91+
AgentDevcontainerSSHButtonProps
92+
> = ({ workspace, container }) => {
93+
const paper = useClassName(classNames.paper, []);
94+
95+
return (
96+
<Popover>
97+
<PopoverTrigger>
98+
<Button
99+
size="small"
100+
variant="text"
101+
endIcon={<KeyboardArrowDown />}
102+
css={{ fontSize: 13, padding: "8px 12px" }}
103+
>
104+
Connect via SSH
105+
</Button>
106+
</PopoverTrigger>
107+
108+
<PopoverContent horizontal="right" classes={{ paper }}>
109+
<HelpTooltipText>
110+
Run the following commands to connect with SSH:
111+
</HelpTooltipText>
112+
113+
<ol style={{ margin: 0, padding: 0 }}>
114+
<Stack spacing={0.5} css={styles.codeExamples}>
115+
<SSHStep
116+
helpText="Connect to the container:"
117+
codeExample={`coder ssh ${workspace} -c ${container}`}
118+
/>
119+
</Stack>
120+
</ol>
121+
122+
<HelpTooltipLinksGroup>
123+
<HelpTooltipLink href={docs("/install")}>
124+
Install Coder CLI
125+
</HelpTooltipLink>
126+
<HelpTooltipLink href={docs("/user-guides/workspace-access#ssh")}>
127+
SSH configuration
128+
</HelpTooltipLink>
129+
</HelpTooltipLinksGroup>
130+
</PopoverContent>
131+
</Popover>
132+
);
133+
};
134+
85135
interface SSHStepProps {
86136
helpText: string;
87137
codeExample: string;

site/src/modules/resources/TerminalLink/TerminalLink.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ export const Language = {
1111
};
1212

1313
export interface TerminalLinkProps {
14-
agentName?: TypesGen.WorkspaceAgent["name"];
15-
userName?: TypesGen.User["username"];
16-
workspaceName: TypesGen.Workspace["name"];
14+
workspaceName: string;
15+
agentName?: string;
16+
userName?: string;
17+
containerName?: string;
1718
}
1819

1920
/**
@@ -27,11 +28,16 @@ export const TerminalLink: FC<TerminalLinkProps> = ({
2728
agentName,
2829
userName = "me",
2930
workspaceName,
31+
containerName,
3032
}) => {
33+
const params = new URLSearchParams();
34+
if (containerName) {
35+
params.append("container", containerName);
36+
}
3137
// Always use the primary for the terminal link. This is a relative link.
3238
const href = `/@${userName}/${workspaceName}${
3339
agentName ? `.${agentName}` : ""
34-
}/terminal`;
40+
}/terminal?${params.toString()}`;
3541

3642
return (
3743
<Link

0 commit comments

Comments
 (0)
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