Skip to content

Commit 2de6dcc

Browse files
feat: add open in vscode button for devcontainers
1 parent cc733ab commit 2de6dcc

File tree

4 files changed

+232
-7
lines changed

4 files changed

+232
-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: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
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 configFile = container.labels["devcontainer.config_file"];
31+
const containerFolder = container.volumes[folderPath];
32+
2433
return (
2534
<section
2635
className="border border-border border-dashed rounded p-6 "
@@ -40,9 +49,18 @@ export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
4049
<h4 className="m-0 text-xl font-semibold">Forwarded ports</h4>
4150

4251
<div className="flex gap-4 flex-wrap mt-4">
52+
<VSCodeDevContainerButton
53+
userName={workspace.owner_name}
54+
workspaceName={workspace.name}
55+
folderPath={folderPath}
56+
devContainerPath={configFile}
57+
devContainerFolder={containerFolder}
58+
displayApps={agent.display_apps}
59+
/>
60+
4361
<TerminalLink
4462
workspaceName={workspace.name}
45-
agentName={agentName}
63+
agentName={agent.name}
4664
containerName={container.name}
4765
userName={workspace.owner_name}
4866
/>
@@ -58,7 +76,7 @@ export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
5876
? portForwardURL(
5977
wildcardHostname,
6078
port.host_port!,
61-
agentName,
79+
agent.name,
6280
workspace.name,
6381
workspace.owner_name,
6482
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
@@ -289,7 +289,7 @@ export const AgentRow: FC<AgentRowProps> = ({
289289
container={container}
290290
workspace={workspace}
291291
wildcardHostname={proxy.preferredWildcardHostname}
292-
agentName={agent.name}
292+
agent={agent}
293293
/>
294294
);
295295
})}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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+
folderPath: string;
18+
devContainerPath: string;
19+
devContainerFolder: string;
20+
displayApps: readonly DisplayApp[];
21+
}
22+
23+
type VSCodeVariant = "vscode" | "vscode-insiders";
24+
25+
const VARIANT_KEY = "vscode-variant";
26+
27+
export const VSCodeDevContainerButton: FC<VSCodeDevContainerButtonProps> = (
28+
props,
29+
) => {
30+
const [isVariantMenuOpen, setIsVariantMenuOpen] = useState(false);
31+
const previousVariant = localStorage.getItem(VARIANT_KEY);
32+
const [variant, setVariant] = useState<VSCodeVariant>(() => {
33+
if (!previousVariant) {
34+
return "vscode";
35+
}
36+
return previousVariant as VSCodeVariant;
37+
});
38+
const menuAnchorRef = useRef<HTMLDivElement>(null);
39+
40+
const selectVariant = (variant: VSCodeVariant) => {
41+
localStorage.setItem(VARIANT_KEY, variant);
42+
setVariant(variant);
43+
setIsVariantMenuOpen(false);
44+
};
45+
46+
const includesVSCodeDesktop = props.displayApps.includes("vscode");
47+
const includesVSCodeInsiders = props.displayApps.includes("vscode_insiders");
48+
49+
return includesVSCodeDesktop && includesVSCodeInsiders ? (
50+
<div>
51+
<ButtonGroup ref={menuAnchorRef} variant="outlined">
52+
{variant === "vscode" ? (
53+
<VSCodeButton {...props} />
54+
) : (
55+
<VSCodeInsidersButton {...props} />
56+
)}
57+
58+
<AgentButton
59+
aria-controls={
60+
isVariantMenuOpen ? "vscode-variant-button-menu" : undefined
61+
}
62+
aria-expanded={isVariantMenuOpen ? "true" : undefined}
63+
aria-label="select VSCode variant"
64+
aria-haspopup="menu"
65+
disableRipple
66+
onClick={() => {
67+
setIsVariantMenuOpen(true);
68+
}}
69+
css={{ paddingLeft: 0, paddingRight: 0 }}
70+
>
71+
<KeyboardArrowDownIcon css={{ fontSize: 16 }} />
72+
</AgentButton>
73+
</ButtonGroup>
74+
75+
<Menu
76+
open={isVariantMenuOpen}
77+
anchorEl={menuAnchorRef.current}
78+
onClose={() => setIsVariantMenuOpen(false)}
79+
css={{
80+
"& .MuiMenu-paper": {
81+
width: menuAnchorRef.current?.clientWidth,
82+
},
83+
}}
84+
>
85+
<MenuItem
86+
css={{ fontSize: 14 }}
87+
onClick={() => {
88+
selectVariant("vscode");
89+
}}
90+
>
91+
<VSCodeIcon css={{ width: 12, height: 12 }} />
92+
{DisplayAppNameMap.vscode}
93+
</MenuItem>
94+
<MenuItem
95+
css={{ fontSize: 14 }}
96+
onClick={() => {
97+
selectVariant("vscode-insiders");
98+
}}
99+
>
100+
<VSCodeInsidersIcon css={{ width: 12, height: 12 }} />
101+
{DisplayAppNameMap.vscode_insiders}
102+
</MenuItem>
103+
</Menu>
104+
</div>
105+
) : includesVSCodeDesktop ? (
106+
<VSCodeButton {...props} />
107+
) : (
108+
<VSCodeInsidersButton {...props} />
109+
);
110+
};
111+
112+
const VSCodeButton: FC<VSCodeDevContainerButtonProps> = ({
113+
userName,
114+
workspaceName,
115+
agentName,
116+
folderPath,
117+
devContainerPath,
118+
devContainerFolder,
119+
}) => {
120+
const [loading, setLoading] = useState(false);
121+
122+
return (
123+
<AgentButton
124+
startIcon={<VSCodeIcon />}
125+
disabled={loading}
126+
onClick={() => {
127+
setLoading(true);
128+
API.getApiKey()
129+
.then(({ key }) => {
130+
const query = new URLSearchParams({
131+
owner: userName,
132+
workspace: workspaceName,
133+
url: location.origin,
134+
token: key,
135+
devContainerPath,
136+
devContainerFolder,
137+
});
138+
if (agentName) {
139+
query.set("agent", agentName);
140+
}
141+
if (folderPath) {
142+
query.set("folder", folderPath);
143+
}
144+
145+
location.href = `vscode://coder.coder-remote/openDevContainer?${query.toString()}`;
146+
})
147+
.catch((ex) => {
148+
console.error(ex);
149+
})
150+
.finally(() => {
151+
setLoading(false);
152+
});
153+
}}
154+
>
155+
{DisplayAppNameMap.vscode}
156+
</AgentButton>
157+
);
158+
};
159+
160+
const VSCodeInsidersButton: FC<VSCodeDevContainerButtonProps> = ({
161+
userName,
162+
workspaceName,
163+
agentName,
164+
folderPath,
165+
devContainerPath,
166+
devContainerFolder,
167+
}) => {
168+
const [loading, setLoading] = useState(false);
169+
170+
return (
171+
<AgentButton
172+
startIcon={<VSCodeInsidersIcon />}
173+
disabled={loading}
174+
onClick={() => {
175+
setLoading(true);
176+
API.getApiKey()
177+
.then(({ key }) => {
178+
const query = new URLSearchParams({
179+
owner: userName,
180+
workspace: workspaceName,
181+
url: location.origin,
182+
token: key,
183+
devContainerPath,
184+
devContainerFolder,
185+
});
186+
if (agentName) {
187+
query.set("agent", agentName);
188+
}
189+
if (folderPath) {
190+
query.set("folder", folderPath);
191+
}
192+
193+
location.href = `vscode-insiders://coder.coder-remote/openDevContainer?${query.toString()}`;
194+
})
195+
.catch((ex) => {
196+
console.error(ex);
197+
})
198+
.finally(() => {
199+
setLoading(false);
200+
});
201+
}}
202+
>
203+
{DisplayAppNameMap.vscode_insiders}
204+
</AgentButton>
205+
);
206+
};

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