Skip to content

Commit ab8c437

Browse files
feat(site): open dev container in vscode (#17182)
Closes #16426 Adds a new button `VSCodeDevContainerButton` for connecting to a dev container with VSCode.
1 parent aa3d71d commit ab8c437

File tree

5 files changed

+281
-7
lines changed

5 files changed

+281
-7
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import {
33
MockWorkspace,
4+
MockWorkspaceAgent,
45
MockWorkspaceAgentContainer,
56
MockWorkspaceAgentContainerPorts,
67
} from "testHelpers/entities";
@@ -13,7 +14,7 @@ const meta: Meta<typeof AgentDevcontainerCard> = {
1314
container: MockWorkspaceAgentContainer,
1415
workspace: MockWorkspace,
1516
wildcardHostname: "*.wildcard.hostname",
16-
agentName: "dev",
17+
agent: MockWorkspaceAgent,
1718
},
1819
};
1920

site/src/modules/resources/AgentDevcontainerCard.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
import Link from "@mui/material/Link";
22
import Tooltip, { type TooltipProps } from "@mui/material/Tooltip";
3-
import type { Workspace, WorkspaceAgentContainer } from "api/typesGenerated";
3+
import type {
4+
Workspace,
5+
WorkspaceAgent,
6+
WorkspaceAgentContainer,
7+
} from "api/typesGenerated";
48
import { ExternalLinkIcon } from "lucide-react";
59
import type { FC } from "react";
610
import { portForwardURL } from "utils/portForward";
711
import { AgentButton } from "./AgentButton";
812
import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton";
913
import { TerminalLink } from "./TerminalLink/TerminalLink";
14+
import { VSCodeDevContainerButton } from "./VSCodeDevContainerButton/VSCodeDevContainerButton";
1015

1116
type AgentDevcontainerCardProps = {
17+
agent: WorkspaceAgent;
1218
container: WorkspaceAgentContainer;
1319
workspace: Workspace;
1420
wildcardHostname: string;
15-
agentName: string;
1621
};
1722

1823
export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
24+
agent,
1925
container,
2026
workspace,
21-
agentName,
2227
wildcardHostname,
2328
}) => {
29+
const folderPath = container.labels["devcontainer.local_folder"];
30+
const containerFolder = container.volumes[folderPath];
31+
2432
return (
2533
<section
2634
className="border border-border border-dashed rounded p-6 "
@@ -40,9 +48,17 @@ export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
4048
<h4 className="m-0 text-xl font-semibold">Forwarded ports</h4>
4149

4250
<div className="flex gap-4 flex-wrap mt-4">
51+
<VSCodeDevContainerButton
52+
userName={workspace.owner_name}
53+
workspaceName={workspace.name}
54+
devContainerName={container.name}
55+
devContainerFolder={containerFolder}
56+
displayApps={agent.display_apps}
57+
/>
58+
4359
<TerminalLink
4460
workspaceName={workspace.name}
45-
agentName={agentName}
61+
agentName={agent.name}
4662
containerName={container.name}
4763
userName={workspace.owner_name}
4864
/>
@@ -58,7 +74,7 @@ export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
5874
? portForwardURL(
5975
wildcardHostname,
6076
port.host_port!,
61-
agentName,
77+
agent.name,
6278
workspace.name,
6379
workspace.owner_name,
6480
location.protocol === "https" ? "https" : "http",

site/src/modules/resources/AgentRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ export const AgentRow: FC<AgentRowProps> = ({
290290
container={container}
291291
workspace={workspace}
292292
wildcardHostname={proxy.preferredWildcardHostname}
293-
agentName={agent.name}
293+
agent={agent}
294294
/>
295295
);
296296
})}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
3+
import { VSCodeDevContainerButton } from "./VSCodeDevContainerButton";
4+
5+
const meta: Meta<typeof VSCodeDevContainerButton> = {
6+
title: "modules/resources/VSCodeDevContainerButton",
7+
component: VSCodeDevContainerButton,
8+
};
9+
10+
export default meta;
11+
type Story = StoryObj<typeof VSCodeDevContainerButton>;
12+
13+
export const Default: Story = {
14+
args: {
15+
userName: MockWorkspace.owner_name,
16+
workspaceName: MockWorkspace.name,
17+
agentName: MockWorkspaceAgent.name,
18+
devContainerName: "musing_ride",
19+
devContainerFolder: "/workspace/coder",
20+
displayApps: [
21+
"vscode",
22+
"vscode_insiders",
23+
"port_forwarding_helper",
24+
"ssh_helper",
25+
"web_terminal",
26+
],
27+
},
28+
};
29+
30+
export const VSCodeOnly: Story = {
31+
args: {
32+
userName: MockWorkspace.owner_name,
33+
workspaceName: MockWorkspace.name,
34+
agentName: MockWorkspaceAgent.name,
35+
devContainerName: "nifty_borg",
36+
devContainerFolder: "/workspace/coder",
37+
displayApps: [
38+
"vscode",
39+
"port_forwarding_helper",
40+
"ssh_helper",
41+
"web_terminal",
42+
],
43+
},
44+
};
45+
46+
export const InsidersOnly: Story = {
47+
args: {
48+
userName: MockWorkspace.owner_name,
49+
workspaceName: MockWorkspace.name,
50+
agentName: MockWorkspaceAgent.name,
51+
devContainerName: "amazing_swartz",
52+
devContainerFolder: "/workspace/coder",
53+
displayApps: [
54+
"vscode_insiders",
55+
"port_forwarding_helper",
56+
"ssh_helper",
57+
"web_terminal",
58+
],
59+
},
60+
};
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
2+
import ButtonGroup from "@mui/material/ButtonGroup";
3+
import Menu from "@mui/material/Menu";
4+
import MenuItem from "@mui/material/MenuItem";
5+
import { API } from "api/api";
6+
import type { DisplayApp } from "api/typesGenerated";
7+
import { VSCodeIcon } from "components/Icons/VSCodeIcon";
8+
import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon";
9+
import { type FC, useRef, useState } from "react";
10+
import { AgentButton } from "../AgentButton";
11+
import { DisplayAppNameMap } from "../AppLink/AppLink";
12+
13+
export interface VSCodeDevContainerButtonProps {
14+
userName: string;
15+
workspaceName: string;
16+
agentName?: string;
17+
devContainerName: string;
18+
devContainerFolder: string;
19+
displayApps: readonly DisplayApp[];
20+
}
21+
22+
type VSCodeVariant = "vscode" | "vscode-insiders";
23+
24+
const VARIANT_KEY = "vscode-variant";
25+
26+
export const VSCodeDevContainerButton: FC<VSCodeDevContainerButtonProps> = (
27+
props,
28+
) => {
29+
const [isVariantMenuOpen, setIsVariantMenuOpen] = useState(false);
30+
const previousVariant = localStorage.getItem(VARIANT_KEY);
31+
const [variant, setVariant] = useState<VSCodeVariant>(() => {
32+
if (!previousVariant) {
33+
return "vscode";
34+
}
35+
return previousVariant as VSCodeVariant;
36+
});
37+
const menuAnchorRef = useRef<HTMLDivElement>(null);
38+
39+
const selectVariant = (variant: VSCodeVariant) => {
40+
localStorage.setItem(VARIANT_KEY, variant);
41+
setVariant(variant);
42+
setIsVariantMenuOpen(false);
43+
};
44+
45+
const includesVSCodeDesktop = props.displayApps.includes("vscode");
46+
const includesVSCodeInsiders = props.displayApps.includes("vscode_insiders");
47+
48+
return includesVSCodeDesktop && includesVSCodeInsiders ? (
49+
<div>
50+
<ButtonGroup ref={menuAnchorRef} variant="outlined">
51+
{variant === "vscode" ? (
52+
<VSCodeButton {...props} />
53+
) : (
54+
<VSCodeInsidersButton {...props} />
55+
)}
56+
57+
<AgentButton
58+
aria-controls={
59+
isVariantMenuOpen ? "vscode-variant-button-menu" : undefined
60+
}
61+
aria-expanded={isVariantMenuOpen ? "true" : undefined}
62+
aria-label="select VSCode variant"
63+
aria-haspopup="menu"
64+
disableRipple
65+
onClick={() => {
66+
setIsVariantMenuOpen(true);
67+
}}
68+
css={{ paddingLeft: 0, paddingRight: 0 }}
69+
>
70+
<KeyboardArrowDownIcon css={{ fontSize: 16 }} />
71+
</AgentButton>
72+
</ButtonGroup>
73+
74+
<Menu
75+
open={isVariantMenuOpen}
76+
anchorEl={menuAnchorRef.current}
77+
onClose={() => setIsVariantMenuOpen(false)}
78+
css={{
79+
"& .MuiMenu-paper": {
80+
width: menuAnchorRef.current?.clientWidth,
81+
},
82+
}}
83+
>
84+
<MenuItem
85+
css={{ fontSize: 14 }}
86+
onClick={() => {
87+
selectVariant("vscode");
88+
}}
89+
>
90+
<VSCodeIcon css={{ width: 12, height: 12 }} />
91+
{DisplayAppNameMap.vscode}
92+
</MenuItem>
93+
<MenuItem
94+
css={{ fontSize: 14 }}
95+
onClick={() => {
96+
selectVariant("vscode-insiders");
97+
}}
98+
>
99+
<VSCodeInsidersIcon css={{ width: 12, height: 12 }} />
100+
{DisplayAppNameMap.vscode_insiders}
101+
</MenuItem>
102+
</Menu>
103+
</div>
104+
) : includesVSCodeDesktop ? (
105+
<VSCodeButton {...props} />
106+
) : (
107+
<VSCodeInsidersButton {...props} />
108+
);
109+
};
110+
111+
const VSCodeButton: FC<VSCodeDevContainerButtonProps> = ({
112+
userName,
113+
workspaceName,
114+
agentName,
115+
devContainerName,
116+
devContainerFolder,
117+
}) => {
118+
const [loading, setLoading] = useState(false);
119+
120+
return (
121+
<AgentButton
122+
startIcon={<VSCodeIcon />}
123+
disabled={loading}
124+
onClick={() => {
125+
setLoading(true);
126+
API.getApiKey()
127+
.then(({ key }) => {
128+
const query = new URLSearchParams({
129+
owner: userName,
130+
workspace: workspaceName,
131+
url: location.origin,
132+
token: key,
133+
devContainerName,
134+
devContainerFolder,
135+
});
136+
if (agentName) {
137+
query.set("agent", agentName);
138+
}
139+
140+
location.href = `vscode://coder.coder-remote/openDevContainer?${query.toString()}`;
141+
})
142+
.catch((ex) => {
143+
console.error(ex);
144+
})
145+
.finally(() => {
146+
setLoading(false);
147+
});
148+
}}
149+
>
150+
{DisplayAppNameMap.vscode}
151+
</AgentButton>
152+
);
153+
};
154+
155+
const VSCodeInsidersButton: FC<VSCodeDevContainerButtonProps> = ({
156+
userName,
157+
workspaceName,
158+
agentName,
159+
devContainerName,
160+
devContainerFolder,
161+
}) => {
162+
const [loading, setLoading] = useState(false);
163+
164+
return (
165+
<AgentButton
166+
startIcon={<VSCodeInsidersIcon />}
167+
disabled={loading}
168+
onClick={() => {
169+
setLoading(true);
170+
API.getApiKey()
171+
.then(({ key }) => {
172+
const query = new URLSearchParams({
173+
owner: userName,
174+
workspace: workspaceName,
175+
url: location.origin,
176+
token: key,
177+
devContainerName,
178+
devContainerFolder,
179+
});
180+
if (agentName) {
181+
query.set("agent", agentName);
182+
}
183+
184+
location.href = `vscode-insiders://coder.coder-remote/openDevContainer?${query.toString()}`;
185+
})
186+
.catch((ex) => {
187+
console.error(ex);
188+
})
189+
.finally(() => {
190+
setLoading(false);
191+
});
192+
}}
193+
>
194+
{DisplayAppNameMap.vscode_insiders}
195+
</AgentButton>
196+
);
197+
};

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