mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: show agent version in UI and CLI (#3709)
This commit adds the ability for agents to set their version upon start. This is then reported in the UI and CLI.
This commit is contained in:
@@ -81,6 +81,7 @@
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/react-helmet": "6.1.5",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/superagent": "4.1.15",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.31.0",
|
||||
@@ -112,6 +113,7 @@
|
||||
"prettier": "2.7.1",
|
||||
"prettier-plugin-organize-imports": "3.0.0",
|
||||
"react-hot-loader": "4.13.0",
|
||||
"semver": "^7.3.7",
|
||||
"sql-formatter": "8.2.0",
|
||||
"style-loader": "3.3.1",
|
||||
"ts-jest": "27.1.4",
|
||||
|
||||
@@ -249,6 +249,11 @@ export interface ParameterSchema {
|
||||
readonly validation_contains?: string[]
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
export interface PostWorkspaceAgentVersionRequest {
|
||||
readonly version: string
|
||||
}
|
||||
|
||||
// From codersdk/provisionerdaemons.go
|
||||
export interface ProvisionerDaemon {
|
||||
readonly id: string
|
||||
@@ -479,6 +484,7 @@ export interface WorkspaceAgent {
|
||||
// Named type "inet.af/netaddr.IPPrefix" unknown, using "any"
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
readonly ipv6: any
|
||||
readonly version: string
|
||||
}
|
||||
|
||||
// From codersdk/workspaceagents.go
|
||||
|
||||
@@ -9,14 +9,15 @@ import useTheme from "@material-ui/styles/useTheme"
|
||||
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
|
||||
import { TableCellDataPrimary } from "components/TableCellData/TableCellData"
|
||||
import { FC } from "react"
|
||||
import { getDisplayAgentStatus } from "util/workspace"
|
||||
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
|
||||
import { getDisplayAgentStatus, getDisplayVersionStatus } from "util/workspace"
|
||||
import { BuildInfoResponse, Workspace, WorkspaceResource } from "../../api/typesGenerated"
|
||||
import { AppLink } from "../AppLink/AppLink"
|
||||
import { SSHButton } from "../SSHButton/SSHButton"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
|
||||
import { TerminalLink } from "../TerminalLink/TerminalLink"
|
||||
import { AgentHelpTooltip } from "../Tooltips/AgentHelpTooltip"
|
||||
import { AgentOutdatedTooltip } from "../Tooltips/AgentOutdatedTooltip"
|
||||
import { ResourcesHelpTooltip } from "../Tooltips/ResourcesHelpTooltip"
|
||||
import { ResourceAvatarData } from "./ResourceAvatarData"
|
||||
|
||||
@@ -26,6 +27,7 @@ const Language = {
|
||||
agentsLabel: "Agents",
|
||||
agentLabel: "Agent",
|
||||
statusLabel: "status: ",
|
||||
versionLabel: "version: ",
|
||||
osLabel: "os: ",
|
||||
}
|
||||
|
||||
@@ -34,6 +36,7 @@ interface ResourcesProps {
|
||||
getResourcesError?: Error | unknown
|
||||
workspace: Workspace
|
||||
canUpdateWorkspace: boolean
|
||||
buildInfo?: BuildInfoResponse | undefined
|
||||
}
|
||||
|
||||
export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
||||
@@ -41,9 +44,11 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
||||
getResourcesError,
|
||||
workspace,
|
||||
canUpdateWorkspace,
|
||||
buildInfo,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const theme: Theme = useTheme()
|
||||
const serverVersion = buildInfo?.version || ""
|
||||
|
||||
return (
|
||||
<div aria-label={Language.resources} className={styles.wrapper}>
|
||||
@@ -90,6 +95,10 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const { displayVersion, outdated } = getDisplayVersionStatus(
|
||||
agent.version,
|
||||
serverVersion,
|
||||
)
|
||||
const agentStatus = getDisplayAgentStatus(theme, agent)
|
||||
return (
|
||||
<TableRow key={`${resource.id}-${agent.id}`}>
|
||||
@@ -114,6 +123,11 @@ export const Resources: FC<React.PropsWithChildren<ResourcesProps>> = ({
|
||||
<strong>{Language.osLabel}</strong>
|
||||
<span className={styles.operatingSystem}>{agent.operating_system}</span>
|
||||
</div>
|
||||
<div className={styles.dataRow}>
|
||||
<strong>{Language.versionLabel}</strong>
|
||||
<span className={styles.agentVersion}>{displayVersion}</span>
|
||||
<AgentOutdatedTooltip outdated={outdated} />
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
@@ -188,6 +202,10 @@ const useStyles = makeStyles((theme) => ({
|
||||
textTransform: "capitalize",
|
||||
},
|
||||
|
||||
agentVersion: {
|
||||
display: "block",
|
||||
},
|
||||
|
||||
accessLinks: {
|
||||
display: "flex",
|
||||
gap: theme.spacing(0.5),
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FC } from "react"
|
||||
import { HelpTooltip, HelpTooltipText, HelpTooltipTitle } from "./HelpTooltip"
|
||||
|
||||
export const Language = {
|
||||
label: "Agent Outdated",
|
||||
text: "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.",
|
||||
}
|
||||
|
||||
interface TooltipProps {
|
||||
outdated: boolean
|
||||
}
|
||||
|
||||
export const AgentOutdatedTooltip: FC<React.PropsWithChildren<TooltipProps>> = ({ outdated }) => {
|
||||
if (!outdated) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>{Language.label}</HelpTooltipTitle>
|
||||
<HelpTooltipText>{Language.text}</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
)
|
||||
}
|
||||
@@ -44,6 +44,7 @@ Started.args = {
|
||||
builds: [Mocks.MockWorkspaceBuild],
|
||||
canUpdateWorkspace: true,
|
||||
workspaceErrors: {},
|
||||
buildInfo: Mocks.MockBuildInfo,
|
||||
}
|
||||
|
||||
export const WithoutUpdateAccess = Template.bind({})
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface WorkspaceProps {
|
||||
builds?: TypesGen.WorkspaceBuild[]
|
||||
canUpdateWorkspace: boolean
|
||||
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
|
||||
buildInfo?: TypesGen.BuildInfoResponse
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,6 +63,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
builds,
|
||||
canUpdateWorkspace,
|
||||
workspaceErrors,
|
||||
buildInfo,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const navigate = useNavigate()
|
||||
@@ -128,6 +130,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
||||
getResourcesError={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
|
||||
workspace={workspace}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
buildInfo={buildInfo}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ describe("WorkspacePage", () => {
|
||||
const agent1Status = await screen.findAllByText(
|
||||
DisplayAgentStatusLanguage[MockWorkspaceAgent.status],
|
||||
)
|
||||
expect(agent1Status.length).toEqual(2)
|
||||
expect(agent1Status.length).toEqual(4)
|
||||
const agent2Status = await screen.findAllByText(
|
||||
DisplayAgentStatusLanguage[MockWorkspaceAgentDisconnected.status],
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { useMachine, useSelector } from "@xstate/react"
|
||||
import { useActor, useMachine, useSelector } from "@xstate/react"
|
||||
import dayjs from "dayjs"
|
||||
import minMax from "dayjs/plugin/minMax"
|
||||
import { FC, useContext, useEffect } from "react"
|
||||
@@ -51,6 +51,7 @@ export const WorkspacePage: FC = () => {
|
||||
const canUpdateWorkspace = !!permissions?.updateWorkspace
|
||||
|
||||
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
|
||||
const [buildInfoState] = useActor(xServices.buildInfoXService)
|
||||
|
||||
const styles = useStyles()
|
||||
|
||||
@@ -133,6 +134,7 @@ export const WorkspacePage: FC = () => {
|
||||
[WorkspaceErrors.BUILD_ERROR]: buildError,
|
||||
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
|
||||
}}
|
||||
buildInfo={buildInfoState.context.buildInfo}
|
||||
/>
|
||||
<DeleteWorkspaceDialog
|
||||
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
||||
|
||||
@@ -305,6 +305,7 @@ export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
|
||||
wireguard_public_key: "",
|
||||
disco_public_key: "",
|
||||
ipv6: "",
|
||||
version: MockBuildInfo.version,
|
||||
}
|
||||
|
||||
export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
|
||||
@@ -312,10 +313,19 @@ export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
|
||||
id: "test-workspace-agent-2",
|
||||
name: "another-workspace-agent",
|
||||
status: "disconnected",
|
||||
version: "",
|
||||
}
|
||||
|
||||
export const MockWorkspaceAgentOutdated: TypesGen.WorkspaceAgent = {
|
||||
...MockWorkspaceAgent,
|
||||
id: "test-workspace-agent-3",
|
||||
name: "an-outdated-workspace-agent",
|
||||
version: "v99.999.9998+abcdef",
|
||||
operating_system: "Windows",
|
||||
}
|
||||
|
||||
export const MockWorkspaceResource: TypesGen.WorkspaceResource = {
|
||||
agents: [MockWorkspaceAgent, MockWorkspaceAgentDisconnected],
|
||||
agents: [MockWorkspaceAgent, MockWorkspaceAgentDisconnected, MockWorkspaceAgentOutdated],
|
||||
created_at: "",
|
||||
id: "test-workspace-resource",
|
||||
job_id: "",
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as TypesGen from "../api/typesGenerated"
|
||||
import * as Mocks from "../testHelpers/entities"
|
||||
import {
|
||||
defaultWorkspaceExtension,
|
||||
getDisplayVersionStatus,
|
||||
getDisplayWorkspaceBuildInitiatedBy,
|
||||
isWorkspaceDeleted,
|
||||
isWorkspaceOn,
|
||||
@@ -128,4 +129,23 @@ describe("util > workspace", () => {
|
||||
expect(getDisplayWorkspaceBuildInitiatedBy(build)).toEqual(initiatedBy)
|
||||
})
|
||||
})
|
||||
|
||||
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 (outdated)", true],
|
||||
["v1.2.4", "v1.2.3", "v1.2.4", false],
|
||||
["foo", "bar", "foo", false],
|
||||
])(
|
||||
`getDisplayVersionStatus(theme, %p, %p) returns (%p, %p)`,
|
||||
(agentVersion, serverVersion, expectedVersion, expectedOutdated) => {
|
||||
const { displayVersion, outdated } = getDisplayVersionStatus(agentVersion, serverVersion)
|
||||
expect(displayVersion).toEqual(expectedVersion)
|
||||
expect(expectedOutdated).toEqual(outdated)
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import dayjs from "dayjs"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import minMax from "dayjs/plugin/minMax"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import semver from "semver"
|
||||
import { WorkspaceBuildTransition } from "../api/types"
|
||||
import * as TypesGen from "../api/typesGenerated"
|
||||
|
||||
@@ -95,6 +96,11 @@ export const DisplayWorkspaceBuildStatusLanguage = {
|
||||
failed: "Failed",
|
||||
}
|
||||
|
||||
export const DisplayAgentVersionLanguage = {
|
||||
unknown: "unknown",
|
||||
outdated: "outdated",
|
||||
}
|
||||
|
||||
export const getDisplayWorkspaceBuildStatus = (
|
||||
theme: Theme,
|
||||
build: TypesGen.WorkspaceBuild,
|
||||
@@ -211,6 +217,28 @@ export const getDisplayAgentStatus = (
|
||||
}
|
||||
}
|
||||
|
||||
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} (${DisplayAgentVersionLanguage.outdated})`,
|
||||
outdated: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
displayVersion: agentVersion,
|
||||
outdated: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {
|
||||
const transition = workspace.latest_build.transition
|
||||
const status = workspace.latest_build.job.status
|
||||
|
||||
@@ -3330,6 +3330,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
|
||||
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
|
||||
|
||||
"@types/semver@^7.3.12":
|
||||
version "7.3.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.12.tgz#920447fdd78d76b19de0438b7f60df3c4a80bf1c"
|
||||
integrity sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==
|
||||
|
||||
"@types/serve-index@^1.9.1":
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.1.tgz#1b5e85370a192c01ec6cec4735cf2917337a6278"
|
||||
|
||||
Reference in New Issue
Block a user