Thanks to visit codestin.com
Credit goes to github.com

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 <[email protected]>
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

+12
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,
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

+33-4
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

+5-5
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

+52-2
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

+10-4
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)