Skip to content

Commit f5582c7

Browse files
committed
feat(site): allow recreating devcontainers and showing dirty status
This change allows showing the devcontainer dirty status in the UI as well as a recreate button to update the devcontainer. Closes #16424
1 parent afaa20e commit f5582c7

File tree

4 files changed

+109
-21
lines changed

4 files changed

+109
-21
lines changed

coderd/workspaceagents.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ func (api *API) workspaceAgentRecreateDevcontainer(rw http.ResponseWriter, r *ht
956956
}
957957
defer release()
958958

959-
err = agentConn.RecreateDevcontainer(ctx, container)
959+
m, err := agentConn.RecreateDevcontainer(ctx, container)
960960
if err != nil {
961961
if errors.Is(err, context.Canceled) {
962962
httpapi.Write(ctx, rw, http.StatusRequestTimeout, codersdk.Response{
@@ -977,7 +977,7 @@ func (api *API) workspaceAgentRecreateDevcontainer(rw http.ResponseWriter, r *ht
977977
return
978978
}
979979

980-
httpapi.Write(ctx, rw, http.StatusNoContent, nil)
980+
httpapi.Write(ctx, rw, http.StatusAccepted, m)
981981
}
982982

983983
// @Summary Get connection info for workspace agent

codersdk/workspaceagents.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -518,16 +518,20 @@ func (c *Client) WorkspaceAgentListContainers(ctx context.Context, agentID uuid.
518518
}
519519

520520
// WorkspaceAgentRecreateDevcontainer recreates the devcontainer with the given ID.
521-
func (c *Client) WorkspaceAgentRecreateDevcontainer(ctx context.Context, agentID uuid.UUID, containerIDOrName string) error {
521+
func (c *Client) WorkspaceAgentRecreateDevcontainer(ctx context.Context, agentID uuid.UUID, containerIDOrName string) (Response, error) {
522522
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/workspaceagents/%s/containers/devcontainers/container/%s/recreate", agentID, containerIDOrName), nil)
523523
if err != nil {
524-
return err
524+
return Response{}, err
525525
}
526526
defer res.Body.Close()
527-
if res.StatusCode != http.StatusNoContent {
528-
return ReadBodyAsError(res)
527+
if res.StatusCode != http.StatusAccepted {
528+
return Response{}, ReadBodyAsError(res)
529529
}
530-
return nil
530+
var m Response
531+
if err := json.NewDecoder(res.Body).Decode(&m); err != nil {
532+
return Response{}, xerrors.Errorf("decode response body: %w", err)
533+
}
534+
return m, nil
531535
}
532536

533537
//nolint:revive // Follow is a control flag on the server as well.

codersdk/workspacesdk/agentconn.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,18 +389,22 @@ func (c *AgentConn) ListContainers(ctx context.Context) (codersdk.WorkspaceAgent
389389

390390
// RecreateDevcontainer recreates a devcontainer with the given container.
391391
// This is a blocking call and will wait for the container to be recreated.
392-
func (c *AgentConn) RecreateDevcontainer(ctx context.Context, containerIDOrName string) error {
392+
func (c *AgentConn) RecreateDevcontainer(ctx context.Context, containerIDOrName string) (codersdk.Response, error) {
393393
ctx, span := tracing.StartSpan(ctx)
394394
defer span.End()
395395
res, err := c.apiRequest(ctx, http.MethodPost, "/api/v0/containers/devcontainers/container/"+containerIDOrName+"/recreate", nil)
396396
if err != nil {
397-
return xerrors.Errorf("do request: %w", err)
397+
return codersdk.Response{}, xerrors.Errorf("do request: %w", err)
398398
}
399399
defer res.Body.Close()
400400
if res.StatusCode != http.StatusAccepted {
401-
return codersdk.ReadBodyAsError(res)
401+
return codersdk.Response{}, codersdk.ReadBodyAsError(res)
402402
}
403-
return nil
403+
var m codersdk.Response
404+
if err := json.NewDecoder(res.Body).Decode(&m); err != nil {
405+
return codersdk.Response{}, xerrors.Errorf("decode response body: %w", err)
406+
}
407+
return m, nil
404408
}
405409

406410
// apiRequest makes a request to the workspace agent's HTTP API server.

site/src/modules/resources/AgentDevcontainerCard.tsx

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,23 @@ import {
99
TooltipProvider,
1010
TooltipTrigger,
1111
} from "components/Tooltip/Tooltip";
12-
import { ExternalLinkIcon } from "lucide-react";
12+
import {
13+
HelpTooltip,
14+
HelpTooltipContent,
15+
HelpTooltipText,
16+
HelpTooltipTitle,
17+
HelpTooltipTrigger,
18+
} from "components/HelpTooltip/HelpTooltip";
19+
import { ExternalLinkIcon, Loader2Icon } from "lucide-react";
1320
import type { FC } from "react";
1421
import { portForwardURL } from "utils/portForward";
1522
import { AgentButton } from "./AgentButton";
1623
import { AgentDevcontainerSSHButton } from "./SSHButton/SSHButton";
1724
import { TerminalLink } from "./TerminalLink/TerminalLink";
1825
import { VSCodeDevContainerButton } from "./VSCodeDevContainerButton/VSCodeDevContainerButton";
26+
import { useState } from "react";
27+
import { Button } from "components/Button/Button";
28+
import { displayError } from "components/GlobalSnackbar/utils";
1929

2030
type AgentDevcontainerCardProps = {
2131
agent: WorkspaceAgent;
@@ -32,24 +42,94 @@ export const AgentDevcontainerCard: FC<AgentDevcontainerCardProps> = ({
3242
}) => {
3343
const folderPath = container.labels["devcontainer.local_folder"];
3444
const containerFolder = container.volumes[folderPath];
45+
const [isRecreating, setIsRecreating] = useState(false);
46+
47+
const handleRecreateDevcontainer = async () => {
48+
setIsRecreating(true);
49+
let recreateSucceeded = false;
50+
try {
51+
const response = await fetch(
52+
`/api/v2/workspaceagents/${agent.id}/containers/devcontainers/container/${container.id}/recreate`,
53+
{
54+
method: "POST",
55+
},
56+
);
57+
if (!response.ok) {
58+
const errorData = await response.json().catch(() => ({}));
59+
throw new Error(
60+
errorData.message || `Failed to recreate: ${response.statusText}`,
61+
);
62+
}
63+
// If the request was accepted (e.g. 202), we mark it as succeeded.
64+
// Once complete, the component will unmount, so the spinner will
65+
// disappear with it.
66+
if (response.status === 202) {
67+
recreateSucceeded = true;
68+
}
69+
} catch (error) {
70+
const errorMessage =
71+
error instanceof Error ? error.message : "An unknown error occurred.";
72+
displayError(`Failed to recreate devcontainer: ${errorMessage}`);
73+
console.error("Failed to recreate devcontainer:", error);
74+
} finally {
75+
if (!recreateSucceeded) {
76+
setIsRecreating(false);
77+
}
78+
}
79+
};
3580

3681
return (
3782
<section
3883
className="border border-border border-dashed rounded p-6 "
3984
key={container.id}
4085
>
41-
<header className="flex justify-between">
42-
<h3 className="m-0 text-xs font-medium text-content-secondary">
43-
{container.name}
44-
</h3>
86+
<header className="flex justify-between items-center mb-4">
87+
<div className="flex items-center gap-2">
88+
<h3 className="m-0 text-xs font-medium text-content-secondary">
89+
{container.name}
90+
</h3>
91+
{container.devcontainer_dirty && (
92+
<HelpTooltip>
93+
<HelpTooltipTrigger className="flex items-center text-xs text-warning-foreground ml-2">
94+
<span>Outdated</span>
95+
</HelpTooltipTrigger>
96+
<HelpTooltipContent>
97+
<HelpTooltipTitle>Devcontainer Outdated</HelpTooltipTitle>
98+
<HelpTooltipText>
99+
Devcontainer configuration has been modified and is outdated.
100+
Recreate to get an up-to-date container.
101+
</HelpTooltipText>
102+
</HelpTooltipContent>
103+
</HelpTooltip>
104+
)}
105+
</div>
45106

46-
<AgentDevcontainerSSHButton
47-
workspace={workspace.name}
48-
container={container.name}
49-
/>
107+
<div className="flex items-center gap-2">
108+
<Button
109+
variant="outline"
110+
size="sm"
111+
className="text-xs font-medium"
112+
onClick={handleRecreateDevcontainer}
113+
disabled={isRecreating}
114+
>
115+
{isRecreating ? (
116+
<>
117+
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
118+
Recreating...
119+
</>
120+
) : (
121+
"Recreate"
122+
)}
123+
</Button>
124+
125+
<AgentDevcontainerSSHButton
126+
workspace={workspace.name}
127+
container={container.name}
128+
/>
129+
</div>
50130
</header>
51131

52-
<h4 className="m-0 text-xl font-semibold">Forwarded ports</h4>
132+
<h4 className="m-0 text-xl font-semibold mb-2">Forwarded ports</h4>
53133

54134
<div className="flex gap-4 flex-wrap mt-4">
55135
<VSCodeDevContainerButton

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