feat: display 'Deprecated' warning for agents using old API version (#11058)

Fixes #10340
This commit is contained in:
Spike Curtis 2023-12-08 20:20:44 +04:00 committed by GitHub
parent 78517cab52
commit 6d66cb246d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 134 additions and 44 deletions

View File

@ -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}>

View File

@ -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",
},
};

View File

@ -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} />

View File

@ -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}
/>
</>

View File

@ -99,6 +99,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element {
agent={agent}
workspace={MockWorkspace}
serverVersion=""
serverAPIVersion=""
onUpdateAgent={action("updateAgent")}
/>
</ProxyContext.Provider>

View File

@ -195,6 +195,7 @@ function getAgentRow(agent: WorkspaceAgent): JSX.Element {
agent={agent}
workspace={MockWorkspace}
serverVersion=""
serverAPIVersion=""
onUpdateAgent={action("updateAgent")}
/>
</ProxyContext.Provider>

View File

@ -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
/>
)}

View File

@ -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",

View File

@ -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);
},
);
});

View File

@ -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 => {