mirror of https://github.com/coder/coder.git
feat: display 'Deprecated' warning for agents using old API version (#11058)
Fixes #10340
This commit is contained in:
parent
78517cab52
commit
6d66cb246d
|
@ -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<typeof HelpPopover> & {
|
||||
agent: WorkspaceAgent;
|
||||
serverVersion: string;
|
||||
status: agentVersionStatus;
|
||||
onUpdate: () => void;
|
||||
};
|
||||
|
||||
export const AgentOutdatedTooltip: FC<AgentOutdatedTooltipProps> = ({
|
||||
agent,
|
||||
serverVersion,
|
||||
status,
|
||||
onUpdate,
|
||||
onOpen,
|
||||
id,
|
||||
|
@ -33,6 +36,18 @@ export const AgentOutdatedTooltip: FC<AgentOutdatedTooltipProps> = ({
|
|||
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 (
|
||||
<HelpPopover
|
||||
|
@ -45,12 +60,8 @@ export const AgentOutdatedTooltip: FC<AgentOutdatedTooltipProps> = ({
|
|||
<HelpTooltipContext.Provider value={{ open, onClose }}>
|
||||
<Stack spacing={1}>
|
||||
<div>
|
||||
<HelpTooltipTitle>Agent Outdated</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
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.
|
||||
</HelpTooltipText>
|
||||
<HelpTooltipTitle>{title}</HelpTooltipTitle>
|
||||
<HelpTooltipText>{text}</HelpTooltipText>
|
||||
</div>
|
||||
|
||||
<Stack spacing={0.5}>
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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<AgentRowProps> = ({
|
|||
hideSSHButton,
|
||||
hideVSCodeDesktopButton,
|
||||
serverVersion,
|
||||
serverAPIVersion,
|
||||
onUpdateAgent,
|
||||
storybookAgentMetadata,
|
||||
sshPrefix,
|
||||
|
@ -179,6 +181,7 @@ export const AgentRow: FC<AgentRowProps> = ({
|
|||
<AgentVersion
|
||||
agent={agent}
|
||||
serverVersion={serverVersion}
|
||||
serverAPIVersion={serverAPIVersion}
|
||||
onUpdate={onUpdateAgent}
|
||||
/>
|
||||
<AgentLatency agent={agent} />
|
||||
|
|
|
@ -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<HTMLButtonElement>(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 <span>Updated</span>;
|
||||
}
|
||||
|
||||
|
@ -27,7 +33,7 @@ export const AgentVersion: FC<{
|
|||
onMouseLeave={() => setIsOpen(false)}
|
||||
css={{ cursor: "pointer" }}
|
||||
>
|
||||
Outdated
|
||||
{status === agentVersionStatus.Outdated ? "Outdated" : "Deprecated"}
|
||||
</span>
|
||||
<AgentOutdatedTooltip
|
||||
id={id}
|
||||
|
@ -37,6 +43,7 @@ export const AgentVersion: FC<{
|
|||
onClose={() => setIsOpen(false)}
|
||||
agent={agent}
|
||||
serverVersion={serverVersion}
|
||||
status={status}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -99,6 +99,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element {
|
|||
agent={agent}
|
||||
workspace={MockWorkspace}
|
||||
serverVersion=""
|
||||
serverAPIVersion=""
|
||||
onUpdateAgent={action("updateAgent")}
|
||||
/>
|
||||
</ProxyContext.Provider>
|
||||
|
|
|
@ -195,6 +195,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element {
|
|||
agent={agent}
|
||||
workspace={MockWorkspace}
|
||||
serverVersion=""
|
||||
serverAPIVersion=""
|
||||
onUpdateAgent={action("updateAgent")}
|
||||
/>
|
||||
</ProxyContext.Provider>
|
||||
|
|
|
@ -357,6 +357,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||
hideSSHButton={hideSSHButton}
|
||||
hideVSCodeDesktopButton={hideVSCodeDesktopButton}
|
||||
serverVersion={buildInfo?.version || ""}
|
||||
serverAPIVersion={buildInfo?.agent_api_version || ""}
|
||||
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -194,7 +194,7 @@ export const MockProxyLatencies: Record<string, ProxyLatencyReport> = {
|
|||
};
|
||||
|
||||
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",
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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 => {
|
||||
|
|
Loading…
Reference in New Issue