Skip to content

Commit 5fe436d

Browse files
committed
feat: add support for external agents in the UI and extend CodeExample
1 parent d36771a commit 5fe436d

File tree

7 files changed

+221
-9
lines changed

7 files changed

+221
-9
lines changed

site/src/api/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2022,6 +2022,16 @@ class ApiMethods {
20222022
return response.data;
20232023
};
20242024

2025+
getWorkspaceAgentCredentials = async (
2026+
workspaceID: string,
2027+
agentName: string,
2028+
): Promise<TypesGen.ExternalAgentCredentials> => {
2029+
const response = await this.axios.get(
2030+
`/api/v2/workspaces/${workspaceID}/external-agent/${agentName}/credentials`,
2031+
);
2032+
return response.data;
2033+
};
2034+
20252035
upsertWorkspaceAgentSharedPort = async (
20262036
workspaceID: string,
20272037
req: TypesGen.UpsertWorkspaceAgentPortShareRequest,

site/src/components/CodeExample/CodeExample.stories.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,12 @@ export const LongCode: Story = {
3131
code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L",
3232
},
3333
};
34+
35+
export const Redact: Story = {
36+
args: {
37+
secret: false,
38+
redactPattern: /CODER_AGENT_TOKEN="([^"]+)"/g,
39+
redactReplacement: `CODER_AGENT_TOKEN="********"`,
40+
showRevealButton: true,
41+
},
42+
};

site/src/components/CodeExample/CodeExample.tsx

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
import type { Interpolation, Theme } from "@emotion/react";
2-
import type { FC } from "react";
2+
import { Button } from "components/Button/Button";
3+
import {
4+
Tooltip,
5+
TooltipContent,
6+
TooltipProvider,
7+
TooltipTrigger,
8+
} from "components/Tooltip/Tooltip";
9+
import { EyeIcon, EyeOffIcon } from "lucide-react";
10+
import { type FC, useState } from "react";
311
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
412
import { CopyButton } from "../CopyButton/CopyButton";
513

614
interface CodeExampleProps {
715
code: string;
16+
/** Defaulting to true to be on the safe side; you should have to opt out of the secure option, not remember to opt in */
817
secret?: boolean;
18+
/** Redact parts of the code if the user doesn't want to obfuscate the whole code */
19+
redactPattern?: RegExp;
20+
/** Replacement text for redacted content */
21+
redactReplacement?: string;
22+
/** Show a button to reveal the redacted parts of the code */
23+
showRevealButton?: boolean;
924
className?: string;
1025
}
1126

@@ -15,11 +30,28 @@ interface CodeExampleProps {
1530
export const CodeExample: FC<CodeExampleProps> = ({
1631
code,
1732
className,
18-
19-
// Defaulting to true to be on the safe side; you should have to opt out of
20-
// the secure option, not remember to opt in
2133
secret = true,
34+
redactPattern,
35+
redactReplacement = "********",
36+
showRevealButton,
2237
}) => {
38+
const [showFullValue, setShowFullValue] = useState(false);
39+
40+
const displayValue = secret
41+
? obfuscateText(code)
42+
: redactPattern && !showFullValue
43+
? code.replace(redactPattern, redactReplacement)
44+
: code;
45+
46+
const showButtonLabel = showFullValue
47+
? "Hide sensitive data"
48+
: "Show sensitive data";
49+
const icon = showFullValue ? (
50+
<EyeOffIcon className="h-4 w-4" />
51+
) : (
52+
<EyeIcon className="h-4 w-4" />
53+
);
54+
2355
return (
2456
<div css={styles.container} className={className}>
2557
<code css={[styles.code, secret && styles.secret]}>
@@ -33,17 +65,36 @@ export const CodeExample: FC<CodeExampleProps> = ({
3365
* 2. Even with it turned on and supported, the plaintext is still
3466
* readily available in the HTML itself
3567
*/}
36-
<span aria-hidden>{obfuscateText(code)}</span>
68+
<span aria-hidden>{displayValue}</span>
3769
<span className="sr-only">
3870
Encrypted text. Please access via the copy button.
3971
</span>
4072
</>
4173
) : (
42-
code
74+
displayValue
4375
)}
4476
</code>
4577

46-
<CopyButton text={code} label="Copy code" />
78+
<div className="flex items-center gap-1">
79+
{showRevealButton && redactPattern && !secret && (
80+
<TooltipProvider>
81+
<Tooltip>
82+
<TooltipTrigger asChild>
83+
<Button
84+
size="icon"
85+
variant="subtle"
86+
onClick={() => setShowFullValue(!showFullValue)}
87+
>
88+
{icon}
89+
<span className="sr-only">{showButtonLabel}</span>
90+
</Button>
91+
</TooltipTrigger>
92+
<TooltipContent>{showButtonLabel}</TooltipContent>
93+
</Tooltip>
94+
</TooltipProvider>
95+
)}
96+
<CopyButton text={code} label="Copy code" />
97+
</div>
4798
</div>
4899
);
49100
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { chromatic } from "testHelpers/chromatic";
3+
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
4+
import { withDashboardProvider } from "testHelpers/storybook";
5+
import { AgentExternal } from "./AgentExternal";
6+
7+
const meta: Meta<typeof AgentExternal> = {
8+
title: "modules/resources/AgentExternal",
9+
component: AgentExternal,
10+
args: {
11+
isExternalAgent: true,
12+
agent: {
13+
...MockWorkspaceAgent,
14+
status: "connecting",
15+
operating_system: "linux",
16+
architecture: "amd64",
17+
},
18+
workspace: MockWorkspace,
19+
},
20+
decorators: [withDashboardProvider],
21+
parameters: {
22+
chromatic,
23+
},
24+
};
25+
26+
export default meta;
27+
type Story = StoryObj<typeof AgentExternal>;
28+
29+
export const Connecting: Story = {
30+
args: {
31+
agent: {
32+
...MockWorkspaceAgent,
33+
status: "connecting",
34+
operating_system: "linux",
35+
architecture: "amd64",
36+
},
37+
},
38+
};
39+
40+
export const Timeout: Story = {
41+
args: {
42+
agent: {
43+
...MockWorkspaceAgent,
44+
status: "timeout",
45+
operating_system: "linux",
46+
architecture: "amd64",
47+
},
48+
},
49+
};
50+
51+
export const DifferentOS: Story = {
52+
args: {
53+
agent: {
54+
...MockWorkspaceAgent,
55+
status: "connecting",
56+
operating_system: "darwin",
57+
architecture: "arm64",
58+
},
59+
},
60+
};
61+
62+
export const NotExternalAgent: Story = {
63+
args: {
64+
isExternalAgent: false,
65+
agent: {
66+
...MockWorkspaceAgent,
67+
status: "connecting",
68+
operating_system: "linux",
69+
architecture: "amd64",
70+
},
71+
},
72+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { API } from "api/api";
2+
import type { Workspace, WorkspaceAgent } from "api/typesGenerated";
3+
import isChromatic from "chromatic/isChromatic";
4+
import { CodeExample } from "components/CodeExample/CodeExample";
5+
import { type FC, useEffect, useState } from "react";
6+
7+
interface AgentExternalProps {
8+
isExternalAgent: boolean;
9+
agent: WorkspaceAgent;
10+
workspace: Workspace;
11+
}
12+
13+
export const AgentExternal: FC<AgentExternalProps> = ({
14+
isExternalAgent,
15+
agent,
16+
workspace,
17+
}) => {
18+
const [externalAgentToken, setExternalAgentToken] = useState<string | null>(
19+
null,
20+
);
21+
const [command, setCommand] = useState<string | null>(null);
22+
23+
const origin = isChromatic() ? "https://example.com" : window.location.origin;
24+
useEffect(() => {
25+
if (
26+
isExternalAgent &&
27+
(agent.status === "timeout" || agent.status === "connecting")
28+
) {
29+
API.getWorkspaceAgentCredentials(workspace.id, agent.name).then((res) => {
30+
setExternalAgentToken(res.agent_token);
31+
setCommand(res.command);
32+
});
33+
}
34+
}, [isExternalAgent, agent.status, workspace.id, agent.name]);
35+
36+
return (
37+
<section className="text-base text-muted-foreground pb-2 leading-relaxed">
38+
<p>
39+
Please run the following command to attach an agent to the{" "}
40+
{workspace.name} workspace:
41+
</p>
42+
<CodeExample
43+
code={command ?? ""}
44+
secret={false}
45+
redactPattern={/CODER_AGENT_TOKEN="([^"]+)"/g}
46+
redactReplacement={`CODER_AGENT_TOKEN="********"`}
47+
showRevealButton={true}
48+
/>
49+
</section>
50+
);
51+
};

site/src/modules/resources/AgentRow.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import AutoSizer from "react-virtualized-auto-sizer";
2727
import type { FixedSizeList as List, ListOnScrollProps } from "react-window";
2828
import { AgentApps, organizeAgentApps } from "./AgentApps/AgentApps";
2929
import { AgentDevcontainerCard } from "./AgentDevcontainerCard";
30+
import { AgentExternal } from "./AgentExternal";
3031
import { AgentLatency } from "./AgentLatency";
3132
import { AGENT_LOG_LINE_HEIGHT } from "./AgentLogs/AgentLogLine";
3233
import { AgentLogs } from "./AgentLogs/AgentLogs";
@@ -62,9 +63,10 @@ export const AgentRow: FC<AgentRowProps> = ({
6263
const appSections = organizeAgentApps(agent.apps);
6364
const hasAppsToDisplay =
6465
!browser_only || appSections.some((it) => it.apps.length > 0);
66+
const isExternalAgent = workspace.latest_build.has_external_agent;
6567
const shouldDisplayAgentApps =
6668
(agent.status === "connected" && hasAppsToDisplay) ||
67-
agent.status === "connecting";
69+
(agent.status === "connecting" && !isExternalAgent);
6870
const hasVSCodeApp =
6971
agent.display_apps.includes("vscode") ||
7072
agent.display_apps.includes("vscode_insiders");
@@ -258,7 +260,7 @@ export const AgentRow: FC<AgentRowProps> = ({
258260
</section>
259261
)}
260262

261-
{agent.status === "connecting" && (
263+
{agent.status === "connecting" && !isExternalAgent && (
262264
<section css={styles.apps}>
263265
<Skeleton
264266
width={80}
@@ -293,6 +295,15 @@ export const AgentRow: FC<AgentRowProps> = ({
293295
</section>
294296
)}
295297

298+
{isExternalAgent &&
299+
(agent.status === "timeout" || agent.status === "connecting") && (
300+
<AgentExternal
301+
isExternalAgent={isExternalAgent}
302+
agent={agent}
303+
workspace={workspace}
304+
/>
305+
)}
306+
296307
<AgentMetadata initialMetadata={initialMetadata} agent={agent} />
297308
</div>
298309

site/src/modules/workspaces/actions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ export const abilitiesByWorkspaceStatus = (
6363
};
6464
}
6565

66+
if (workspace.latest_build.has_external_agent) {
67+
return {
68+
actions: [],
69+
canCancel: false,
70+
canAcceptJobs: true,
71+
};
72+
}
73+
6674
const status = workspace.latest_build.status;
6775

6876
switch (status) {

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