Skip to content

Commit 6d66cb2

Browse files
authored
feat: display 'Deprecated' warning for agents using old API version (#11058)
Fixes #10340
1 parent 78517ca commit 6d66cb2

File tree

10 files changed

+134
-44
lines changed

10 files changed

+134
-44
lines changed

site/src/components/Resources/AgentOutdatedTooltip.tsx

+20-9
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,28 @@ import { type ComponentProps, type FC } from "react";
22
import { useTheme } from "@emotion/react";
33
import RefreshIcon from "@mui/icons-material/RefreshOutlined";
44
import {
5-
HelpTooltipText,
65
HelpPopover,
7-
HelpTooltipTitle,
86
HelpTooltipAction,
9-
HelpTooltipLinksGroup,
107
HelpTooltipContext,
8+
HelpTooltipLinksGroup,
9+
HelpTooltipText,
10+
HelpTooltipTitle,
1111
} from "components/HelpTooltip/HelpTooltip";
1212
import type { WorkspaceAgent } from "api/typesGenerated";
1313
import { Stack } from "components/Stack/Stack";
14+
import { agentVersionStatus } from "../../utils/workspace";
1415

1516
type AgentOutdatedTooltipProps = ComponentProps<typeof HelpPopover> & {
1617
agent: WorkspaceAgent;
1718
serverVersion: string;
19+
status: agentVersionStatus;
1820
onUpdate: () => void;
1921
};
2022

2123
export const AgentOutdatedTooltip: FC<AgentOutdatedTooltipProps> = ({
2224
agent,
2325
serverVersion,
26+
status,
2427
onUpdate,
2528
onOpen,
2629
id,
@@ -33,6 +36,18 @@ export const AgentOutdatedTooltip: FC<AgentOutdatedTooltipProps> = ({
3336
fontWeight: 600,
3437
color: theme.palette.text.primary,
3538
};
39+
const title =
40+
status === agentVersionStatus.Outdated
41+
? "Agent Outdated"
42+
: "Agent Deprecated";
43+
const opener =
44+
status === agentVersionStatus.Outdated
45+
? "This agent is an older version than the Coder server."
46+
: "This agent is using a deprecated version of the API.";
47+
const text =
48+
opener +
49+
" This can happen after you update Coder with running workspaces. " +
50+
"To fix this, you can stop and start the workspace.";
3651

3752
return (
3853
<HelpPopover
@@ -45,12 +60,8 @@ export const AgentOutdatedTooltip: FC<AgentOutdatedTooltipProps> = ({
4560
<HelpTooltipContext.Provider value={{ open, onClose }}>
4661
<Stack spacing={1}>
4762
<div>
48-
<HelpTooltipTitle>Agent Outdated</HelpTooltipTitle>
49-
<HelpTooltipText>
50-
This agent is an older version than the Coder server. This can
51-
happen after you update Coder with running workspaces. To fix
52-
this, you can stop and start the workspace.
53-
</HelpTooltipText>
63+
<HelpTooltipTitle>{title}</HelpTooltipTitle>
64+
<HelpTooltipText>{text}</HelpTooltipText>
5465
</div>
5566

5667
<Stack spacing={0.5}>

site/src/components/Resources/AgentRow.stories.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {
1414
MockWorkspaceAgentStarting,
1515
MockWorkspaceAgentStartTimeout,
1616
MockWorkspaceAgentTimeout,
17+
MockWorkspaceAgentLogSource,
18+
MockWorkspaceAgentDeprecated,
1719
MockWorkspaceApp,
1820
MockProxyLatencies,
19-
MockWorkspaceAgentLogSource,
2021
} from "testHelpers/entities";
2122
import { AgentRow, LineWithID } from "./AgentRow";
2223
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
@@ -287,5 +288,15 @@ export const Outdated: Story = {
287288
agent: MockWorkspaceAgentOutdated,
288289
workspace: MockWorkspace,
289290
serverVersion: "v99.999.9999+c1cdf14",
291+
serverAPIVersion: "1.0",
292+
},
293+
};
294+
295+
export const Deprecated: Story = {
296+
args: {
297+
agent: MockWorkspaceAgentDeprecated,
298+
workspace: MockWorkspace,
299+
serverVersion: "v99.999.9999+c1cdf14",
300+
serverAPIVersion: "2.0",
290301
},
291302
};

site/src/components/Resources/AgentRow.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface AgentRowProps {
5555
hideSSHButton?: boolean;
5656
hideVSCodeDesktopButton?: boolean;
5757
serverVersion: string;
58+
serverAPIVersion: string;
5859
onUpdateAgent: () => void;
5960
storybookLogs?: LineWithID[];
6061
storybookAgentMetadata?: WorkspaceAgentMetadata[];
@@ -68,6 +69,7 @@ export const AgentRow: FC<AgentRowProps> = ({
6869
hideSSHButton,
6970
hideVSCodeDesktopButton,
7071
serverVersion,
72+
serverAPIVersion,
7173
onUpdateAgent,
7274
storybookAgentMetadata,
7375
sshPrefix,
@@ -179,6 +181,7 @@ export const AgentRow: FC<AgentRowProps> = ({
179181
<AgentVersion
180182
agent={agent}
181183
serverVersion={serverVersion}
184+
serverAPIVersion={serverAPIVersion}
182185
onUpdate={onUpdateAgent}
183186
/>
184187
<AgentLatency agent={agent} />

site/src/components/Resources/AgentVersion.tsx

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
import { type FC, useRef, useState } from "react";
22
import type { WorkspaceAgent } from "api/typesGenerated";
3-
import { getDisplayVersionStatus } from "utils/workspace";
3+
import { agentVersionStatus, getDisplayVersionStatus } from "utils/workspace";
44
import { AgentOutdatedTooltip } from "./AgentOutdatedTooltip";
55

66
export const AgentVersion: FC<{
77
agent: WorkspaceAgent;
88
serverVersion: string;
9+
serverAPIVersion: string;
910
onUpdate: () => void;
10-
}> = ({ agent, serverVersion, onUpdate }) => {
11+
}> = ({ agent, serverVersion, serverAPIVersion, onUpdate }) => {
1112
const anchorRef = useRef<HTMLButtonElement>(null);
1213
const [isOpen, setIsOpen] = useState(false);
1314
const id = isOpen ? "version-outdated-popover" : undefined;
14-
const { outdated } = getDisplayVersionStatus(agent.version, serverVersion);
15+
const { status } = getDisplayVersionStatus(
16+
agent.version,
17+
serverVersion,
18+
agent.api_version,
19+
serverAPIVersion,
20+
);
1521

16-
if (!outdated) {
22+
if (status === agentVersionStatus.Updated) {
1723
return <span>Updated</span>;
1824
}
1925

@@ -27,7 +33,7 @@ export const AgentVersion: FC<{
2733
onMouseLeave={() => setIsOpen(false)}
2834
css={{ cursor: "pointer" }}
2935
>
30-
Outdated
36+
{status === agentVersionStatus.Outdated ? "Outdated" : "Deprecated"}
3137
</span>
3238
<AgentOutdatedTooltip
3339
id={id}
@@ -37,6 +43,7 @@ export const AgentVersion: FC<{
3743
onClose={() => setIsOpen(false)}
3844
agent={agent}
3945
serverVersion={serverVersion}
46+
status={status}
4047
onUpdate={onUpdate}
4148
/>
4249
</>

site/src/components/Resources/ResourceCard.stories.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element {
9999
agent={agent}
100100
workspace={MockWorkspace}
101101
serverVersion=""
102+
serverAPIVersion=""
102103
onUpdateAgent={action("updateAgent")}
103104
/>
104105
</ProxyContext.Provider>

site/src/components/Resources/Resources.stories.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element {
195195
agent={agent}
196196
workspace={MockWorkspace}
197197
serverVersion=""
198+
serverAPIVersion=""
198199
onUpdateAgent={action("updateAgent")}
199200
/>
200201
</ProxyContext.Provider>

site/src/pages/WorkspacePage/Workspace.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
357357
hideSSHButton={hideSSHButton}
358358
hideVSCodeDesktopButton={hideVSCodeDesktopButton}
359359
serverVersion={buildInfo?.version || ""}
360+
serverAPIVersion={buildInfo?.agent_api_version || ""}
360361
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
361362
/>
362363
)}

site/src/testHelpers/entities.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export const MockProxyLatencies: Record<string, ProxyLatencyReport> = {
194194
};
195195

196196
export const MockBuildInfo: TypesGen.BuildInfoResponse = {
197-
agent_api_version: "2.1",
197+
agent_api_version: "1.0",
198198
external_url: "file:///mock-url",
199199
version: "v99.999.9999+c9cdf14",
200200
dashboard_url: "https:///mock-url",
@@ -645,6 +645,31 @@ export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = {
645645
lifecycle_state: "ready",
646646
};
647647

648+
export const MockWorkspaceAgentDeprecated: TypesGen.WorkspaceAgent = {
649+
...MockWorkspaceAgent,
650+
id: "test-workspace-agent-3",
651+
name: "an-outdated-workspace-agent",
652+
version: "v99.999.9998+abcdef",
653+
api_version: "1.99",
654+
operating_system: "Windows",
655+
latency: {
656+
...MockWorkspaceAgent.latency,
657+
Chicago: {
658+
preferred: false,
659+
latency_ms: 95.11,
660+
},
661+
"San Francisco": {
662+
preferred: false,
663+
latency_ms: 111.55,
664+
},
665+
Paris: {
666+
preferred: false,
667+
latency_ms: 221.66,
668+
},
669+
},
670+
lifecycle_state: "ready",
671+
};
672+
648673
export const MockWorkspaceAgentConnecting: TypesGen.WorkspaceAgent = {
649674
...MockWorkspaceAgent,
650675
id: "test-workspace-agent-connecting",

site/src/utils/workspace.test.ts

+30-12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import dayjs from "dayjs";
22
import * as TypesGen from "api/typesGenerated";
33
import * as Mocks from "testHelpers/entities";
44
import {
5+
agentVersionStatus,
56
defaultWorkspaceExtension,
67
getDisplayVersionStatus,
78
getDisplayWorkspaceBuildInitiatedBy,
@@ -101,23 +102,40 @@ describe("util > workspace", () => {
101102
});
102103

103104
describe("getDisplayVersionStatus", () => {
104-
it.each<[string, string, string, boolean]>([
105-
["", "", "Unknown", false],
106-
["", "v1.2.3", "Unknown", false],
107-
["v1.2.3", "", "v1.2.3", false],
108-
["v1.2.3", "v1.2.3", "v1.2.3", false],
109-
["v1.2.3", "v1.2.4", "v1.2.3", true],
110-
["v1.2.4", "v1.2.3", "v1.2.4", false],
111-
["foo", "bar", "foo", false],
105+
it.each<[string, string, string, string, string, agentVersionStatus]>([
106+
["", "", "", "", "Unknown", agentVersionStatus.Updated],
107+
["", "v1.2.3", "", "", "Unknown", agentVersionStatus.Updated],
108+
["v1.2.3", "", "", "", "v1.2.3", agentVersionStatus.Updated],
109+
["v1.2.3", "v1.2.3", "", "", "v1.2.3", agentVersionStatus.Updated],
110+
["v1.2.3", "v1.2.4", "", "", "v1.2.3", agentVersionStatus.Outdated],
111+
["v1.2.4", "v1.2.3", "", "", "v1.2.4", agentVersionStatus.Updated],
112+
["foo", "bar", "", "", "foo", agentVersionStatus.Updated],
113+
[
114+
"v1.2.3",
115+
"v1.2.4",
116+
"1.8",
117+
"2.1",
118+
"v1.2.3",
119+
agentVersionStatus.Deprecated,
120+
],
112121
])(
113-
`getDisplayVersionStatus(theme, %p, %p) returns (%p, %p)`,
114-
(agentVersion, serverVersion, expectedVersion, expectedOutdated) => {
115-
const { displayVersion, outdated } = getDisplayVersionStatus(
122+
`getDisplayVersionStatus(theme, %p, %p, %p, %p) returns (%p, %p)`,
123+
(
124+
agentVersion,
125+
serverVersion,
126+
agentAPIVersion,
127+
serverAPIVersion,
128+
expectedVersion,
129+
expectedStatus,
130+
) => {
131+
const { displayVersion, status } = getDisplayVersionStatus(
116132
agentVersion,
117133
serverVersion,
134+
agentAPIVersion,
135+
serverAPIVersion,
118136
);
119137
expect(displayVersion).toEqual(expectedVersion);
120-
expect(expectedOutdated).toEqual(outdated);
138+
expect(status).toEqual(expectedStatus);
121139
},
122140
);
123141
});

site/src/utils/workspace.tsx

+28-16
Original file line numberDiff line numberDiff line change
@@ -108,26 +108,38 @@ export const displayWorkspaceBuildDuration = (
108108
return duration ? `${duration} seconds` : inProgressLabel;
109109
};
110110

111+
export const enum agentVersionStatus {
112+
Updated = 1,
113+
Outdated = 2,
114+
Deprecated = 3,
115+
}
116+
111117
export const getDisplayVersionStatus = (
112118
agentVersion: string,
113119
serverVersion: string,
114-
): { displayVersion: string; outdated: boolean } => {
115-
if (!semver.valid(serverVersion) || !semver.valid(agentVersion)) {
116-
return {
117-
displayVersion: agentVersion || DisplayAgentVersionLanguage.unknown,
118-
outdated: false,
119-
};
120-
} else if (semver.lt(agentVersion, serverVersion)) {
121-
return {
122-
displayVersion: agentVersion,
123-
outdated: true,
124-
};
125-
} else {
126-
return {
127-
displayVersion: agentVersion,
128-
outdated: false,
129-
};
120+
agentAPIVersion: string,
121+
serverAPIVersion: string,
122+
): { displayVersion: string; status: agentVersionStatus } => {
123+
// APIVersions only have major.minor so coerce them to major.minor.0, so we can use semver.major()
124+
const a = semver.coerce(agentAPIVersion);
125+
const s = semver.coerce(serverAPIVersion);
126+
let status = agentVersionStatus.Updated;
127+
if (
128+
semver.valid(agentVersion) &&
129+
semver.valid(serverVersion) &&
130+
semver.lt(agentVersion, serverVersion)
131+
) {
132+
status = agentVersionStatus.Outdated;
133+
}
134+
// deprecated overrides and implies Outdated
135+
if (a !== null && s !== null && semver.major(a) < semver.major(s)) {
136+
status = agentVersionStatus.Deprecated;
130137
}
138+
const displayVersion = agentVersion || DisplayAgentVersionLanguage.unknown;
139+
return {
140+
displayVersion: displayVersion,
141+
status: status,
142+
};
131143
};
132144

133145
export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {

0 commit comments

Comments
 (0)