diff --git a/site/src/components/Resources/AgentOutdatedTooltip.tsx b/site/src/components/Resources/AgentOutdatedTooltip.tsx index 6bbe9061d1..8901b22385 100644 --- a/site/src/components/Resources/AgentOutdatedTooltip.tsx +++ b/site/src/components/Resources/AgentOutdatedTooltip.tsx @@ -2,25 +2,28 @@ import { type ComponentProps, type FC } from "react"; import { useTheme } from "@emotion/react"; import RefreshIcon from "@mui/icons-material/RefreshOutlined"; import { - HelpTooltipText, HelpPopover, - HelpTooltipTitle, HelpTooltipAction, - HelpTooltipLinksGroup, HelpTooltipContext, + HelpTooltipLinksGroup, + HelpTooltipText, + HelpTooltipTitle, } from "components/HelpTooltip/HelpTooltip"; import type { WorkspaceAgent } from "api/typesGenerated"; import { Stack } from "components/Stack/Stack"; +import { agentVersionStatus } from "../../utils/workspace"; type AgentOutdatedTooltipProps = ComponentProps & { agent: WorkspaceAgent; serverVersion: string; + status: agentVersionStatus; onUpdate: () => void; }; export const AgentOutdatedTooltip: FC = ({ agent, serverVersion, + status, onUpdate, onOpen, id, @@ -33,6 +36,18 @@ export const AgentOutdatedTooltip: FC = ({ fontWeight: 600, color: theme.palette.text.primary, }; + const title = + status === agentVersionStatus.Outdated + ? "Agent Outdated" + : "Agent Deprecated"; + const opener = + status === agentVersionStatus.Outdated + ? "This agent is an older version than the Coder server." + : "This agent is using a deprecated version of the API."; + const text = + opener + + " This can happen after you update Coder with running workspaces. " + + "To fix this, you can stop and start the workspace."; return ( = ({
- Agent Outdated - - This agent is an older version than the Coder server. This can - happen after you update Coder with running workspaces. To fix - this, you can stop and start the workspace. - + {title} + {text}
diff --git a/site/src/components/Resources/AgentRow.stories.tsx b/site/src/components/Resources/AgentRow.stories.tsx index 1c73aa7f78..f4a08edbed 100644 --- a/site/src/components/Resources/AgentRow.stories.tsx +++ b/site/src/components/Resources/AgentRow.stories.tsx @@ -14,9 +14,10 @@ import { MockWorkspaceAgentStarting, MockWorkspaceAgentStartTimeout, MockWorkspaceAgentTimeout, + MockWorkspaceAgentLogSource, + MockWorkspaceAgentDeprecated, MockWorkspaceApp, MockProxyLatencies, - MockWorkspaceAgentLogSource, } from "testHelpers/entities"; import { AgentRow, LineWithID } from "./AgentRow"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; @@ -287,5 +288,15 @@ export const Outdated: Story = { agent: MockWorkspaceAgentOutdated, workspace: MockWorkspace, serverVersion: "v99.999.9999+c1cdf14", + serverAPIVersion: "1.0", + }, +}; + +export const Deprecated: Story = { + args: { + agent: MockWorkspaceAgentDeprecated, + workspace: MockWorkspace, + serverVersion: "v99.999.9999+c1cdf14", + serverAPIVersion: "2.0", }, }; diff --git a/site/src/components/Resources/AgentRow.tsx b/site/src/components/Resources/AgentRow.tsx index 4bff9c76db..a908f1caac 100644 --- a/site/src/components/Resources/AgentRow.tsx +++ b/site/src/components/Resources/AgentRow.tsx @@ -55,6 +55,7 @@ export interface AgentRowProps { hideSSHButton?: boolean; hideVSCodeDesktopButton?: boolean; serverVersion: string; + serverAPIVersion: string; onUpdateAgent: () => void; storybookLogs?: LineWithID[]; storybookAgentMetadata?: WorkspaceAgentMetadata[]; @@ -68,6 +69,7 @@ export const AgentRow: FC = ({ hideSSHButton, hideVSCodeDesktopButton, serverVersion, + serverAPIVersion, onUpdateAgent, storybookAgentMetadata, sshPrefix, @@ -179,6 +181,7 @@ export const AgentRow: FC = ({ diff --git a/site/src/components/Resources/AgentVersion.tsx b/site/src/components/Resources/AgentVersion.tsx index 98b358a92a..e353802145 100644 --- a/site/src/components/Resources/AgentVersion.tsx +++ b/site/src/components/Resources/AgentVersion.tsx @@ -1,19 +1,25 @@ import { type FC, useRef, useState } from "react"; import type { WorkspaceAgent } from "api/typesGenerated"; -import { getDisplayVersionStatus } from "utils/workspace"; +import { agentVersionStatus, getDisplayVersionStatus } from "utils/workspace"; import { AgentOutdatedTooltip } from "./AgentOutdatedTooltip"; export const AgentVersion: FC<{ agent: WorkspaceAgent; serverVersion: string; + serverAPIVersion: string; onUpdate: () => void; -}> = ({ agent, serverVersion, onUpdate }) => { +}> = ({ agent, serverVersion, serverAPIVersion, onUpdate }) => { const anchorRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const id = isOpen ? "version-outdated-popover" : undefined; - const { outdated } = getDisplayVersionStatus(agent.version, serverVersion); + const { status } = getDisplayVersionStatus( + agent.version, + serverVersion, + agent.api_version, + serverAPIVersion, + ); - if (!outdated) { + if (status === agentVersionStatus.Updated) { return Updated; } @@ -27,7 +33,7 @@ export const AgentVersion: FC<{ onMouseLeave={() => setIsOpen(false)} css={{ cursor: "pointer" }} > - Outdated + {status === agentVersionStatus.Outdated ? "Outdated" : "Deprecated"} setIsOpen(false)} agent={agent} serverVersion={serverVersion} + status={status} onUpdate={onUpdate} /> diff --git a/site/src/components/Resources/ResourceCard.stories.tsx b/site/src/components/Resources/ResourceCard.stories.tsx index 2759ab4bd5..576ef68db7 100644 --- a/site/src/components/Resources/ResourceCard.stories.tsx +++ b/site/src/components/Resources/ResourceCard.stories.tsx @@ -99,6 +99,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element { agent={agent} workspace={MockWorkspace} serverVersion="" + serverAPIVersion="" onUpdateAgent={action("updateAgent")} /> diff --git a/site/src/components/Resources/Resources.stories.tsx b/site/src/components/Resources/Resources.stories.tsx index 32cde805cf..0a7693f5b4 100644 --- a/site/src/components/Resources/Resources.stories.tsx +++ b/site/src/components/Resources/Resources.stories.tsx @@ -195,6 +195,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element { agent={agent} workspace={MockWorkspace} serverVersion="" + serverAPIVersion="" onUpdateAgent={action("updateAgent")} /> diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 0aa7a3d742..08b278bec5 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -357,6 +357,7 @@ export const Workspace: FC> = ({ hideSSHButton={hideSSHButton} hideVSCodeDesktopButton={hideVSCodeDesktopButton} serverVersion={buildInfo?.version || ""} + serverAPIVersion={buildInfo?.agent_api_version || ""} onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated /> )} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index bab25c74e7..139df91c2e 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -194,7 +194,7 @@ export const MockProxyLatencies: Record = { }; export const MockBuildInfo: TypesGen.BuildInfoResponse = { - agent_api_version: "2.1", + agent_api_version: "1.0", external_url: "file:///mock-url", version: "v99.999.9999+c9cdf14", dashboard_url: "https:///mock-url", @@ -645,6 +645,31 @@ export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = { lifecycle_state: "ready", }; +export const MockWorkspaceAgentDeprecated: TypesGen.WorkspaceAgent = { + ...MockWorkspaceAgent, + id: "test-workspace-agent-3", + name: "an-outdated-workspace-agent", + version: "v99.999.9998+abcdef", + api_version: "1.99", + operating_system: "Windows", + latency: { + ...MockWorkspaceAgent.latency, + Chicago: { + preferred: false, + latency_ms: 95.11, + }, + "San Francisco": { + preferred: false, + latency_ms: 111.55, + }, + Paris: { + preferred: false, + latency_ms: 221.66, + }, + }, + lifecycle_state: "ready", +}; + export const MockWorkspaceAgentConnecting: TypesGen.WorkspaceAgent = { ...MockWorkspaceAgent, id: "test-workspace-agent-connecting", diff --git a/site/src/utils/workspace.test.ts b/site/src/utils/workspace.test.ts index f7385eb850..3d267acf5f 100644 --- a/site/src/utils/workspace.test.ts +++ b/site/src/utils/workspace.test.ts @@ -2,6 +2,7 @@ import dayjs from "dayjs"; import * as TypesGen from "api/typesGenerated"; import * as Mocks from "testHelpers/entities"; import { + agentVersionStatus, defaultWorkspaceExtension, getDisplayVersionStatus, getDisplayWorkspaceBuildInitiatedBy, @@ -101,23 +102,40 @@ describe("util > workspace", () => { }); describe("getDisplayVersionStatus", () => { - it.each<[string, string, string, boolean]>([ - ["", "", "Unknown", false], - ["", "v1.2.3", "Unknown", false], - ["v1.2.3", "", "v1.2.3", false], - ["v1.2.3", "v1.2.3", "v1.2.3", false], - ["v1.2.3", "v1.2.4", "v1.2.3", true], - ["v1.2.4", "v1.2.3", "v1.2.4", false], - ["foo", "bar", "foo", false], + it.each<[string, string, string, string, string, agentVersionStatus]>([ + ["", "", "", "", "Unknown", agentVersionStatus.Updated], + ["", "v1.2.3", "", "", "Unknown", agentVersionStatus.Updated], + ["v1.2.3", "", "", "", "v1.2.3", agentVersionStatus.Updated], + ["v1.2.3", "v1.2.3", "", "", "v1.2.3", agentVersionStatus.Updated], + ["v1.2.3", "v1.2.4", "", "", "v1.2.3", agentVersionStatus.Outdated], + ["v1.2.4", "v1.2.3", "", "", "v1.2.4", agentVersionStatus.Updated], + ["foo", "bar", "", "", "foo", agentVersionStatus.Updated], + [ + "v1.2.3", + "v1.2.4", + "1.8", + "2.1", + "v1.2.3", + agentVersionStatus.Deprecated, + ], ])( - `getDisplayVersionStatus(theme, %p, %p) returns (%p, %p)`, - (agentVersion, serverVersion, expectedVersion, expectedOutdated) => { - const { displayVersion, outdated } = getDisplayVersionStatus( + `getDisplayVersionStatus(theme, %p, %p, %p, %p) returns (%p, %p)`, + ( + agentVersion, + serverVersion, + agentAPIVersion, + serverAPIVersion, + expectedVersion, + expectedStatus, + ) => { + const { displayVersion, status } = getDisplayVersionStatus( agentVersion, serverVersion, + agentAPIVersion, + serverAPIVersion, ); expect(displayVersion).toEqual(expectedVersion); - expect(expectedOutdated).toEqual(outdated); + expect(status).toEqual(expectedStatus); }, ); }); diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index db68597e1a..ac0eb1407a 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -108,26 +108,38 @@ export const displayWorkspaceBuildDuration = ( return duration ? `${duration} seconds` : inProgressLabel; }; +export const enum agentVersionStatus { + Updated = 1, + Outdated = 2, + Deprecated = 3, +} + export const getDisplayVersionStatus = ( agentVersion: string, serverVersion: string, -): { displayVersion: string; outdated: boolean } => { - if (!semver.valid(serverVersion) || !semver.valid(agentVersion)) { - return { - displayVersion: agentVersion || DisplayAgentVersionLanguage.unknown, - outdated: false, - }; - } else if (semver.lt(agentVersion, serverVersion)) { - return { - displayVersion: agentVersion, - outdated: true, - }; - } else { - return { - displayVersion: agentVersion, - outdated: false, - }; + agentAPIVersion: string, + serverAPIVersion: string, +): { displayVersion: string; status: agentVersionStatus } => { + // APIVersions only have major.minor so coerce them to major.minor.0, so we can use semver.major() + const a = semver.coerce(agentAPIVersion); + const s = semver.coerce(serverAPIVersion); + let status = agentVersionStatus.Updated; + if ( + semver.valid(agentVersion) && + semver.valid(serverVersion) && + semver.lt(agentVersion, serverVersion) + ) { + status = agentVersionStatus.Outdated; } + // deprecated overrides and implies Outdated + if (a !== null && s !== null && semver.major(a) < semver.major(s)) { + status = agentVersionStatus.Deprecated; + } + const displayVersion = agentVersion || DisplayAgentVersionLanguage.unknown; + return { + displayVersion: displayVersion, + status: status, + }; }; export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {