diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ea97a5b46a2ef..966c8902c3e73 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -2022,6 +2022,16 @@ class ApiMethods { return response.data; }; + getWorkspaceAgentCredentials = async ( + workspaceID: string, + agentName: string, + ): Promise => { + const response = await this.axios.get( + `/api/v2/workspaces/${workspaceID}/external-agent/${agentName}/credentials`, + ); + return response.data; + }; + upsertWorkspaceAgentSharedPort = async ( workspaceID: string, req: TypesGen.UpsertWorkspaceAgentPortShareRequest, diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 536925a97390f..bcfb07b75452b 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -430,3 +430,13 @@ export const updateWorkspaceACL = (workspaceId: string) => { }, }; }; + +export const workspaceAgentCredentials = ( + workspaceId: string, + agentName: string, +) => { + return { + queryKey: ["workspaces", workspaceId, "agents", agentName, "credentials"], + queryFn: () => API.getWorkspaceAgentCredentials(workspaceId, agentName), + }; +}; diff --git a/site/src/components/CodeExample/CodeExample.stories.tsx b/site/src/components/CodeExample/CodeExample.stories.tsx index 0213762fd31e2..61f129f448a73 100644 --- a/site/src/components/CodeExample/CodeExample.stories.tsx +++ b/site/src/components/CodeExample/CodeExample.stories.tsx @@ -31,3 +31,12 @@ export const LongCode: Story = { code: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnKzATuWwmmt5+CKTPuRGN0R1PBemA+6/SStpLiyX+L", }, }; + +export const Redact: Story = { + args: { + secret: false, + redactPattern: /CODER_AGENT_TOKEN="([^"]+)"/g, + redactReplacement: `CODER_AGENT_TOKEN="********"`, + showRevealButton: true, + }, +}; diff --git a/site/src/components/CodeExample/CodeExample.tsx b/site/src/components/CodeExample/CodeExample.tsx index 474dcb1fac225..b69a220550958 100644 --- a/site/src/components/CodeExample/CodeExample.tsx +++ b/site/src/components/CodeExample/CodeExample.tsx @@ -1,11 +1,26 @@ import type { Interpolation, Theme } from "@emotion/react"; -import type { FC } from "react"; +import { Button } from "components/Button/Button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; +import { EyeIcon, EyeOffIcon } from "lucide-react"; +import { type FC, useState } from "react"; import { MONOSPACE_FONT_FAMILY } from "theme/constants"; import { CopyButton } from "../CopyButton/CopyButton"; interface CodeExampleProps { code: string; + /** Defaulting to true to be on the safe side; you should have to opt out of the secure option, not remember to opt in */ secret?: boolean; + /** Redact parts of the code if the user doesn't want to obfuscate the whole code */ + redactPattern?: RegExp; + /** Replacement text for redacted content */ + redactReplacement?: string; + /** Show a button to reveal the redacted parts of the code */ + showRevealButton?: boolean; className?: string; } @@ -15,11 +30,28 @@ interface CodeExampleProps { export const CodeExample: FC = ({ code, className, - - // Defaulting to true to be on the safe side; you should have to opt out of - // the secure option, not remember to opt in secret = true, + redactPattern, + redactReplacement = "********", + showRevealButton, }) => { + const [showFullValue, setShowFullValue] = useState(false); + + const displayValue = secret + ? obfuscateText(code) + : redactPattern && !showFullValue + ? code.replace(redactPattern, redactReplacement) + : code; + + const showButtonLabel = showFullValue + ? "Hide sensitive data" + : "Show sensitive data"; + const icon = showFullValue ? ( + + ) : ( + + ); + return (
@@ -33,17 +65,36 @@ export const CodeExample: FC = ({ * 2. Even with it turned on and supported, the plaintext is still * readily available in the HTML itself */} - {obfuscateText(code)} + {displayValue} Encrypted text. Please access via the copy button. ) : ( - code + displayValue )} - +
+ {showRevealButton && redactPattern && !secret && ( + + + + + + {showButtonLabel} + + + )} + +
); }; diff --git a/site/src/modules/resources/AgentExternal.stories.tsx b/site/src/modules/resources/AgentExternal.stories.tsx new file mode 100644 index 0000000000000..f83095dd1b570 --- /dev/null +++ b/site/src/modules/resources/AgentExternal.stories.tsx @@ -0,0 +1,70 @@ +import { chromatic } from "testHelpers/chromatic"; +import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities"; +import { withDashboardProvider } from "testHelpers/storybook"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { AgentExternal } from "./AgentExternal"; + +const meta: Meta = { + title: "modules/resources/AgentExternal", + component: AgentExternal, + args: { + agent: { + ...MockWorkspaceAgent, + status: "connecting", + operating_system: "linux", + architecture: "amd64", + }, + workspace: MockWorkspace, + }, + decorators: [withDashboardProvider], + parameters: { + chromatic, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Connecting: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connecting", + operating_system: "linux", + architecture: "amd64", + }, + }, +}; + +export const Timeout: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "timeout", + operating_system: "linux", + architecture: "amd64", + }, + }, +}; + +export const DifferentOS: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connecting", + operating_system: "darwin", + architecture: "arm64", + }, + }, +}; + +export const NotExternalAgent: Story = { + args: { + agent: { + ...MockWorkspaceAgent, + status: "connecting", + operating_system: "linux", + architecture: "amd64", + }, + }, +}; diff --git a/site/src/modules/resources/AgentExternal.tsx b/site/src/modules/resources/AgentExternal.tsx new file mode 100644 index 0000000000000..aa100335fc54a --- /dev/null +++ b/site/src/modules/resources/AgentExternal.tsx @@ -0,0 +1,45 @@ +import { workspaceAgentCredentials } from "api/queries/workspaces"; +import type { Workspace, WorkspaceAgent } from "api/typesGenerated"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; +import { CodeExample } from "components/CodeExample/CodeExample"; +import { Loader } from "components/Loader/Loader"; +import type { FC } from "react"; +import { useQuery } from "react-query"; + +interface AgentExternalProps { + agent: WorkspaceAgent; + workspace: Workspace; +} + +export const AgentExternal: FC = ({ agent, workspace }) => { + const { + data: credentials, + error, + isLoading, + isError, + } = useQuery(workspaceAgentCredentials(workspace.id, agent.name)); + + if (isLoading) { + return ; + } + + if (isError) { + return ; + } + + return ( +
+

+ Please run the following command to attach an agent to the{" "} + {workspace.name} workspace: +

+ +
+ ); +}; diff --git a/site/src/modules/resources/AgentRow.tsx b/site/src/modules/resources/AgentRow.tsx index 495dd01f123f2..6dd52bb31b5bb 100644 --- a/site/src/modules/resources/AgentRow.tsx +++ b/site/src/modules/resources/AgentRow.tsx @@ -27,6 +27,7 @@ import AutoSizer from "react-virtualized-auto-sizer"; import type { FixedSizeList as List, ListOnScrollProps } from "react-window"; import { AgentApps, organizeAgentApps } from "./AgentApps/AgentApps"; import { AgentDevcontainerCard } from "./AgentDevcontainerCard"; +import { AgentExternal } from "./AgentExternal"; import { AgentLatency } from "./AgentLatency"; import { AGENT_LOG_LINE_HEIGHT } from "./AgentLogs/AgentLogLine"; import { AgentLogs } from "./AgentLogs/AgentLogs"; @@ -58,13 +59,14 @@ export const AgentRow: FC = ({ onUpdateAgent, initialMetadata, }) => { - const { browser_only } = useFeatureVisibility(); + const { browser_only, workspace_external_agent } = useFeatureVisibility(); const appSections = organizeAgentApps(agent.apps); const hasAppsToDisplay = !browser_only || appSections.some((it) => it.apps.length > 0); + const isExternalAgent = workspace.latest_build.has_external_agent; const shouldDisplayAgentApps = (agent.status === "connected" && hasAppsToDisplay) || - agent.status === "connecting"; + (agent.status === "connecting" && !isExternalAgent); const hasVSCodeApp = agent.display_apps.includes("vscode") || agent.display_apps.includes("vscode_insiders"); @@ -258,7 +260,7 @@ export const AgentRow: FC = ({ )} - {agent.status === "connecting" && ( + {agent.status === "connecting" && !isExternalAgent && (
= ({
)} + {isExternalAgent && + (agent.status === "timeout" || agent.status === "connecting") && + workspace_external_agent && ( + + )} + diff --git a/site/src/modules/workspaces/actions.ts b/site/src/modules/workspaces/actions.ts index 8b17d3e937c74..533cf981ed6d8 100644 --- a/site/src/modules/workspaces/actions.ts +++ b/site/src/modules/workspaces/actions.ts @@ -63,6 +63,14 @@ export const abilitiesByWorkspaceStatus = ( }; } + if (workspace.latest_build.has_external_agent) { + return { + actions: [], + canCancel: false, + canAcceptJobs: true, + }; + } + const status = workspace.latest_build.status; switch (status) { 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