feat(site): Display workspace build error + option to retry in debug mode (#6903)

This commit is contained in:
Bruno Quaresma
2023-03-31 14:01:12 +00:00
committed by GitHub
parent 901045a95f
commit 90e2bab078
12 changed files with 722 additions and 90 deletions
+18 -4
View File
@@ -489,15 +489,29 @@ export const postWorkspaceBuild = async (
export const startWorkspace = (
workspaceId: string,
templateVersionID: string,
logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"],
) =>
postWorkspaceBuild(workspaceId, {
transition: "start",
template_version_id: templateVersionID,
log_level: logLevel,
})
export const stopWorkspace = (
workspaceId: string,
logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"],
) =>
postWorkspaceBuild(workspaceId, {
transition: "stop",
log_level: logLevel,
})
export const deleteWorkspace = (
workspaceId: string,
logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"],
) =>
postWorkspaceBuild(workspaceId, {
transition: "delete",
log_level: logLevel,
})
export const stopWorkspace = (workspaceId: string) =>
postWorkspaceBuild(workspaceId, { transition: "stop" })
export const deleteWorkspace = (workspaceId: string) =>
postWorkspaceBuild(workspaceId, { transition: "delete" })
export const cancelWorkspaceBuild = async (
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
@@ -60,9 +60,14 @@ export const AlertBanner: FC<React.PropsWithChildren<AlertBannerProps>> = ({
spacing={0}
justifyContent="space-between"
>
<Stack direction="row" alignItems="center" spacing={1}>
<Stack
direction="row"
alignItems="center"
spacing={2}
className={classes.fullWidth}
>
{severityConstants[severity].icon}
<Stack spacing={0}>
<Stack spacing={0} className={classes.fullWidth}>
{children}
{alertMessage}
{detail && (
@@ -94,11 +99,11 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
borderColor: severityConstants[props.severity].color,
border: `1px solid ${colors.orange[7]}`,
borderRadius: theme.shape.borderRadius,
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
padding: theme.spacing(2),
backgroundColor: `${colors.gray[16]}`,
textAlign: "left",
"& span": {
"& > span": {
paddingTop: `${theme.spacing(0.25)}px`,
},
@@ -108,4 +113,8 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
marginRight: `${theme.spacing(1)}px`,
},
}),
fullWidth: {
width: "100%",
},
}))
@@ -13,8 +13,7 @@ export const severityConstants: Record<
color: colors.orange[7],
icon: (
<ReportProblemOutlinedIcon
fontSize="small"
style={{ color: colors.orange[7] }}
style={{ color: colors.orange[7], fontSize: 16 }}
/>
),
},
@@ -22,15 +21,12 @@ export const severityConstants: Record<
color: colors.red[7],
icon: (
<ErrorOutlineOutlinedIcon
fontSize="small"
style={{ color: colors.red[7] }}
style={{ color: colors.red[7], fontSize: 16 }}
/>
),
},
info: {
color: colors.blue[7],
icon: (
<InfoOutlinedIcon fontSize="small" style={{ color: colors.blue[7] }} />
),
icon: <InfoOutlinedIcon style={{ color: colors.blue[7], fontSize: 16 }} />,
},
}
+2 -2
View File
@@ -93,7 +93,7 @@ const useStyles = makeStyles<
background: theme.palette.background.default,
},
scrollWrapper: {
width: "fit-content",
minWidth: "fit-content",
},
line: {
wordBreak: "break-all",
@@ -109,7 +109,7 @@ const useStyles = makeStyles<
},
"&.debug": {
backgroundColor: theme.palette.grey[900],
backgroundColor: theme.palette.background.paperLight,
},
"&.warn": {
@@ -1,5 +1,6 @@
import { action } from "@storybook/addon-actions"
import { Story } from "@storybook/react"
import { ProvisionerJobLog } from "api/typesGenerated"
import * as Mocks from "../../testHelpers/entities"
import { Workspace, WorkspaceErrors, WorkspaceProps } from "./Workspace"
@@ -73,6 +74,23 @@ Failed.args = {
},
}
export const FailedWithLogs = Template.bind({})
FailedWithLogs.args = {
...Running.args,
workspace: {
...Mocks.MockFailedWorkspace,
latest_build: {
...Mocks.MockFailedWorkspace.latest_build,
job: {
...Mocks.MockFailedWorkspace.latest_build.job,
error:
"recv workspace provision: plan terraform: terraform plan: exit status 1",
},
},
},
failedBuildLogs: makeFailedBuildLogs(),
}
export const Deleting = Template.bind({})
Deleting.args = {
...Running.args,
@@ -122,3 +140,494 @@ CancellationError.args = {
}),
},
}
function makeFailedBuildLogs(): ProvisionerJobLog[] {
return [
{
id: 2362,
created_at: "2023-03-21T15:57:42.637Z",
log_source: "provisioner_daemon",
log_level: "info",
stage: "Setting up",
output: "",
},
{
id: 2363,
created_at: "2023-03-21T15:57:42.674Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2364,
created_at: "2023-03-21T15:57:42.674Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "Initializing the backend...",
},
{
id: 2365,
created_at: "2023-03-21T15:57:42.674Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2366,
created_at: "2023-03-21T15:57:42.674Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "Initializing provider plugins...",
},
{
id: 2367,
created_at: "2023-03-21T15:57:42.674Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: '- Finding coder/coder versions matching "~\u003e 0.6.17"...',
},
{
id: 2368,
created_at: "2023-03-21T15:57:42.84Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
'- Finding kreuzwerker/docker versions matching "~\u003e 3.0.1"...',
},
{
id: 2369,
created_at: "2023-03-21T15:57:42.986Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"- Using kreuzwerker/docker v3.0.2 from the shared cache directory",
},
{
id: 2370,
created_at: "2023-03-21T15:57:43.03Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "- Using coder/coder v0.6.20 from the shared cache directory",
},
{
id: 2371,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2372,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"Terraform has created a lock file .terraform.lock.hcl to record the provider",
},
{
id: 2373,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"selections it made above. Include this file in your version control repository",
},
{
id: 2374,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"so that Terraform can guarantee to make the same selections by default when",
},
{
id: 2375,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: 'you run "terraform init" in the future.',
},
{
id: 2376,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2377,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2378,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "Warning: Incomplete lock file information for providers",
},
{
id: 2379,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2380,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"Due to your customized provider installation methods, Terraform was forced to",
},
{
id: 2381,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"calculate lock file checksums locally for the following providers:",
},
{
id: 2382,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: " - coder/coder",
},
{
id: 2383,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: " - kreuzwerker/docker",
},
{
id: 2384,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2385,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"The current .terraform.lock.hcl file only includes checksums for linux_amd64,",
},
{
id: 2386,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"so Terraform running on another platform will fail to install these",
},
{
id: 2387,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "providers.",
},
{
id: 2388,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2389,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "To calculate additional checksums for another platform, run:",
},
{
id: 2390,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: " terraform providers lock -platform=linux_amd64",
},
{
id: 2391,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "(where linux_amd64 is the platform to generate)",
},
{
id: 2392,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2393,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "Terraform has been successfully initialized!",
},
{
id: 2394,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2395,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
'You may now begin working with Terraform. Try running "terraform plan" to see',
},
{
id: 2396,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"any changes that are required for your infrastructure. All Terraform commands",
},
{
id: 2397,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "should now work.",
},
{
id: 2398,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "",
},
{
id: 2399,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"If you ever set or change modules or backend configuration for Terraform,",
},
{
id: 2400,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output:
"rerun this command to reinitialize your working directory. If you forget, other",
},
{
id: 2401,
created_at: "2023-03-21T15:57:43.059Z",
log_source: "provisioner",
log_level: "debug",
stage: "Planning infrastructure",
output: "commands will detect it and remind you to do so if necessary.",
},
{
id: 2402,
created_at: "2023-03-21T15:57:43.078Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output: "Terraform 1.3.4",
},
{
id: 2403,
created_at: "2023-03-21T15:57:43.401Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output: "data.coder_provisioner.me: Refreshing...",
},
{
id: 2404,
created_at: "2023-03-21T15:57:43.402Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output: "data.coder_workspace.me: Refreshing...",
},
{
id: 2405,
created_at: "2023-03-21T15:57:43.402Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output: "data.coder_parameter.security_groups: Refreshing...",
},
{
id: 2406,
created_at: "2023-03-21T15:57:43.402Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output:
"data.coder_provisioner.me: Refresh complete after 0s [id=993f697b-3948-4d31-8377-6c86edc90a83]",
},
{
id: 2407,
created_at: "2023-03-21T15:57:43.403Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output:
"data.coder_workspace.me: Refresh complete after 0s [id=ca18ddca-14b5-4f5f-be55-7bfd2e3c2dc9]",
},
{
id: 2408,
created_at: "2023-03-21T15:57:43.403Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output:
"data.coder_parameter.security_groups: Refresh complete after 0s [id=9832a15f-267b-4abf-9c23-e4265af0befa]",
},
{
id: 2409,
created_at: "2023-03-21T15:57:43.405Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output:
"coder_agent.main: Refreshing state... [id=6c3718cb-605b-4b68-b26f-46dba8767f43]",
},
{
id: 2410,
created_at: "2023-03-21T15:57:43.406Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output:
"coder_agent.main: Refresh complete [id=6c3718cb-605b-4b68-b26f-46dba8767f43]",
},
{
id: 2411,
created_at: "2023-03-21T15:57:43.407Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output:
"docker_volume.home_volume: Refreshing state... [id=coder-ca18ddca-14b5-4f5f-be55-7bfd2e3c2dc9-home]",
},
{
id: 2412,
created_at: "2023-03-21T15:57:43.41Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output:
"coder_app.code-server: Refreshing state... [id=4a45a1cc-9861-4a9c-bd2f-3a2f1abc4c65]",
},
{
id: 2413,
created_at: "2023-03-21T15:57:43.411Z",
log_source: "provisioner",
log_level: "info",
stage: "Planning infrastructure",
output:
"coder_app.code-server: Refresh complete [id=4a45a1cc-9861-4a9c-bd2f-3a2f1abc4c65]",
},
{
id: 2414,
created_at: "2023-03-21T15:57:43.417Z",
log_source: "provisioner",
log_level: "error",
stage: "Planning infrastructure",
output:
"Error: Unable to inspect volume: Error: No such volume: coder-ca18ddca-14b5-4f5f-be55-7bfd2e3c2dc9-home",
},
{
id: 2415,
created_at: "2023-03-21T15:57:43.418Z",
log_source: "provisioner",
log_level: "error",
stage: "Planning infrastructure",
output: 'on main.tf line 61, in resource "docker_volume" "home_volume":',
},
{
id: 2416,
created_at: "2023-03-21T15:57:43.418Z",
log_source: "provisioner",
log_level: "error",
stage: "Planning infrastructure",
output: ' 61: resource "docker_volume" "home_volume" {',
},
{
id: 2417,
created_at: "2023-03-21T15:57:43.418Z",
log_source: "provisioner",
log_level: "error",
stage: "Planning infrastructure",
output: "",
},
{
id: 2418,
created_at: "2023-03-21T15:57:43.419Z",
log_source: "provisioner",
log_level: "error",
stage: "Planning infrastructure",
output: "",
},
{
id: 2419,
created_at: "2023-03-21T15:57:43.422Z",
log_source: "provisioner_daemon",
log_level: "info",
stage: "Cleaning Up",
output: "",
},
]
}
@@ -1,12 +1,16 @@
import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import RefreshOutlined from "@material-ui/icons/RefreshOutlined"
import { Avatar } from "components/Avatar/Avatar"
import { AgentRow } from "components/Resources/AgentRow"
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
import {
ActiveTransition,
WorkspaceBuildProgress,
} from "components/WorkspaceBuildProgress/WorkspaceBuildProgress"
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { AlertBanner } from "../AlertBanner/AlertBanner"
@@ -48,6 +52,7 @@ export interface WorkspaceProps {
resources?: TypesGen.WorkspaceResource[]
builds?: TypesGen.WorkspaceBuild[]
canUpdateWorkspace: boolean
canUpdateTemplate: boolean
hideSSHButton?: boolean
hideVSCodeDesktopButton?: boolean
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
@@ -56,6 +61,8 @@ export interface WorkspaceProps {
sshPrefix?: string
template?: TypesGen.Template
quota_budget?: number
failedBuildLogs: TypesGen.ProvisionerJobLog[] | undefined
handleBuildRetry: () => void
}
/**
@@ -74,6 +81,7 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
resources,
builds,
canUpdateWorkspace,
canUpdateTemplate,
workspaceErrors,
hideSSHButton,
hideVSCodeDesktopButton,
@@ -82,10 +90,13 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
sshPrefix,
template,
quota_budget,
failedBuildLogs,
handleBuildRetry,
}) => {
const styles = useStyles()
const navigate = useNavigate()
const serverVersion = buildInfo?.version || ""
const { t } = useTranslation("workspacePage")
const buildError = Boolean(workspaceErrors[WorkspaceErrors.BUILD_ERROR]) && (
<AlertBanner
@@ -179,6 +190,40 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
handleUpdate={handleUpdate}
/>
{failedBuildLogs && (
<Stack>
<AlertBanner severity="error">
<Stack
className={styles.fullWidth}
direction="row"
alignItems="center"
justifyContent="space-between"
>
<Stack spacing={0}>
<span>Workspace build failed</span>
<span className={styles.errorDetails}>
{workspace.latest_build.job.error}
</span>
</Stack>
{canUpdateTemplate && (
<div>
<Button
onClick={handleBuildRetry}
startIcon={<RefreshOutlined />}
size="small"
variant="outlined"
>
{t("actionButton.retryDebugMode")}
</Button>
</div>
)}
</Stack>
</AlertBanner>
<WorkspaceBuildLogs logs={failedBuildLogs} />
</Stack>
)}
{transitionStats !== undefined && (
<WorkspaceBuildProgress
workspace={workspace}
@@ -255,5 +300,14 @@ export const useStyles = makeStyles((theme) => {
logs: {
border: `1px solid ${theme.palette.divider}`,
},
errorDetails: {
color: theme.palette.text.secondary,
fontSize: 12,
},
fullWidth: {
width: "100%",
},
}
})
@@ -104,6 +104,7 @@ const useStyles = makeStyles<
padding: theme.spacing(2),
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3),
borderTop: `1px solid ${theme.palette.divider}`,
borderBottom: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.paper,
display: "flex",
@@ -113,6 +114,7 @@ const useStyles = makeStyles<
"&:first-child": {
borderTopLeftRadius: theme.shape.borderRadius,
borderTopRightRadius: theme.shape.borderRadius,
borderTop: 0,
},
"&:last-child": {
@@ -121,6 +123,10 @@ const useStyles = makeStyles<
borderBottomLeftRadius: theme.shape.borderRadius,
borderBottomRightRadius: theme.shape.borderRadius,
},
"& + $header": {
borderTop: 0,
},
},
duration: {
+2 -1
View File
@@ -28,7 +28,8 @@
"starting": "Starting...",
"stopping": "Stopping...",
"deleting": "Deleting...",
"settings": "Settings"
"settings": "Settings",
"retryDebugMode": "Try again in debug mode"
},
"disabledButton": {
"canceling": "Canceling",
+34 -22
View File
@@ -1,22 +1,46 @@
import { makeStyles } from "@material-ui/core/styles"
import { useQuery } from "@tanstack/react-query"
import { useMachine } from "@xstate/react"
import { getWorkspaceBuildLogs } from "api/api"
import { Workspace } from "api/typesGenerated"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
import { Loader } from "components/Loader/Loader"
import { FC, useEffect } from "react"
import { FC, useRef } from "react"
import { useParams } from "react-router-dom"
import { firstOrItem } from "util/array"
import { quotaMachine } from "xServices/quotas/quotasXService"
import { workspaceMachine } from "xServices/workspace/workspaceXService"
import { WorkspaceReadyPage } from "./WorkspaceReadyPage"
import { RequirePermission } from "components/RequirePermission/RequirePermission"
const useFailedBuildLogs = (workspace: Workspace | undefined) => {
const now = useRef(new Date())
return useQuery({
queryKey: ["logs", workspace?.latest_build.id],
queryFn: () => {
if (!workspace) {
throw new Error(
`Build log query being called before workspace is defined`,
)
}
return getWorkspaceBuildLogs(workspace.latest_build.id, now.current)
},
enabled: workspace?.latest_build.job.error !== undefined,
})
}
export const WorkspacePage: FC = () => {
const { username: usernameQueryParam, workspace: workspaceQueryParam } =
useParams()
const username = firstOrItem(usernameQueryParam, null)
const workspaceName = firstOrItem(workspaceQueryParam, null)
const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
const { username, workspace: workspaceName } = useParams() as {
username: string
workspace: string
}
const [workspaceState, workspaceSend] = useMachine(workspaceMachine, {
context: {
workspaceName,
username,
},
})
const {
workspace,
getWorkspaceError,
@@ -24,23 +48,10 @@ export const WorkspacePage: FC = () => {
getTemplateParametersWarning,
checkPermissionsError,
} = workspaceState.context
const [quotaState, quotaSend] = useMachine(quotaMachine)
const [quotaState] = useMachine(quotaMachine, { context: { username } })
const { getQuotaError } = quotaState.context
const styles = useStyles()
/**
* Get workspace, template, and organization on mount and whenever workspaceId changes.
* workspaceSend should not change.
*/
useEffect(() => {
username &&
workspaceName &&
workspaceSend({ type: "GET_WORKSPACE", username, workspaceName })
}, [username, workspaceName, workspaceSend])
useEffect(() => {
username && quotaSend({ type: "GET_QUOTA", username })
}, [username, quotaSend])
const failedBuildLogs = useFailedBuildLogs(workspace)
return (
<RequirePermission
@@ -77,6 +88,7 @@ export const WorkspacePage: FC = () => {
}
>
<WorkspaceReadyPage
failedBuildLogs={failedBuildLogs.data}
workspaceState={workspaceState}
quotaState={quotaState}
workspaceSend={workspaceSend}
@@ -1,4 +1,5 @@
import { useActor } from "@xstate/react"
import { ProvisionerJobLog } from "api/typesGenerated"
import { useDashboard } from "components/Dashboard/DashboardProvider"
import dayjs from "dayjs"
import { useFeatureVisibility } from "hooks/useFeatureVisibility"
@@ -31,11 +32,13 @@ interface WorkspaceReadyPageProps {
workspaceState: StateFrom<typeof workspaceMachine>
quotaState: StateFrom<typeof quotaMachine>
workspaceSend: (event: WorkspaceEvent) => void
failedBuildLogs: ProvisionerJobLog[] | undefined
}
export const WorkspaceReadyPage = ({
workspaceState,
quotaState,
failedBuildLogs,
workspaceSend,
}: WorkspaceReadyPageProps): JSX.Element => {
const [_, bannerSend] = useActor(
@@ -60,6 +63,7 @@ export const WorkspaceReadyPage = ({
}
const deadline = getDeadline(workspace)
const canUpdateWorkspace = Boolean(permissions?.updateWorkspace)
const canUpdateTemplate = Boolean(permissions?.updateTemplate)
const { t } = useTranslation("workspacePage")
const favicon = getFaviconByStatus(workspace.latest_build)
const navigate = useNavigate()
@@ -86,6 +90,7 @@ export const WorkspaceReadyPage = ({
</Helmet>
<Workspace
failedBuildLogs={failedBuildLogs}
scheduleProps={{
onDeadlineMinus: (hours: number) => {
bannerSend({
@@ -113,9 +118,11 @@ export const WorkspaceReadyPage = ({
handleUpdate={() => workspaceSend({ type: "UPDATE" })}
handleCancel={() => workspaceSend({ type: "CANCEL" })}
handleSettings={() => navigate("settings")}
handleBuildRetry={() => workspaceSend({ type: "RETRY_BUILD" })}
resources={workspace.latest_build.resources}
builds={builds}
canUpdateWorkspace={canUpdateWorkspace}
canUpdateTemplate={canUpdateTemplate}
hideSSHButton={featureVisibility["browser_only"]}
hideVSCodeDesktopButton={featureVisibility["browser_only"]}
workspaceErrors={{
+4 -14
View File
@@ -3,15 +3,11 @@ import * as API from "../../api/api"
import { WorkspaceQuota } from "../../api/typesGenerated"
export type QuotaContext = {
username: string
quota?: WorkspaceQuota
getQuotaError?: Error | unknown
}
export type QuotaEvent = {
type: "GET_QUOTA"
username: string
}
export const quotaMachine = createMachine(
{
id: "quotasMachine",
@@ -19,21 +15,15 @@ export const quotaMachine = createMachine(
tsTypes: {} as import("./quotasXService.typegen").Typegen0,
schema: {
context: {} as QuotaContext,
events: {} as QuotaEvent,
services: {
getQuota: {
data: {} as WorkspaceQuota,
},
},
},
context: {},
initial: "idle",
initial: "gettingQuotas",
states: {
idle: {
on: {
GET_QUOTA: "gettingQuotas",
},
},
idle: {},
gettingQuotas: {
entry: "clearGetQuotaError",
invoke: {
@@ -67,7 +57,7 @@ export const quotaMachine = createMachine(
}),
},
services: {
getQuota: (context, event) => API.getWorkspaceQuota(event.username),
getQuota: ({ username }) => API.getWorkspaceQuota(username),
},
},
)
@@ -52,6 +52,9 @@ const Language = {
type Permissions = Record<keyof ReturnType<typeof permissionsToCheck>, boolean>
export interface WorkspaceContext {
// Initial data
username: string
workspaceName: string
// our server side events instance
eventSource?: EventSource
workspace?: TypesGen.Workspace
@@ -73,12 +76,13 @@ export interface WorkspaceContext {
checkPermissionsError?: Error | unknown
// applications
applicationsHost?: string
// debug
createBuildLogLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"]
// SSH Config
sshPrefix?: string
}
export type WorkspaceEvent =
| { type: "GET_WORKSPACE"; workspaceName: string; username: string }
| { type: "REFRESH_WORKSPACE"; data: TypesGen.ServerSentEvent["data"] }
| { type: "START" }
| { type: "STOP" }
@@ -95,13 +99,18 @@ export type WorkspaceEvent =
| { type: "EVENT_SOURCE_ERROR"; error: Error | unknown }
| { type: "INCREASE_DEADLINE"; hours: number }
| { type: "DECREASE_DEADLINE"; hours: number }
| { type: "RETRY_BUILD" }
export const checks = {
readWorkspace: "readWorkspace",
updateWorkspace: "updateWorkspace",
updateTemplate: "updateTemplate",
} as const
const permissionsToCheck = (workspace: TypesGen.Workspace) => ({
const permissionsToCheck = (
workspace: TypesGen.Workspace,
template: TypesGen.Template,
) => ({
[checks.readWorkspace]: {
object: {
resource_type: "workspace",
@@ -118,6 +127,13 @@ const permissionsToCheck = (workspace: TypesGen.Workspace) => ({
},
action: "update",
},
[checks.updateTemplate]: {
object: {
resource_type: "template",
resource_id: template.id,
},
action: "update",
},
})
export const workspaceMachine = createMachine(
@@ -170,17 +186,8 @@ export const workspaceMachine = createMachine(
}
},
},
initial: "idle",
on: {
GET_WORKSPACE: {
target: ".gettingWorkspace",
internal: false,
},
},
initial: "gettingWorkspace",
states: {
idle: {
tags: "loading",
},
gettingWorkspace: {
entry: ["clearContext"],
invoke: {
@@ -284,6 +291,23 @@ export const workspaceMachine = createMachine(
ASK_DELETE: "askingDelete",
UPDATE: "requestingUpdate",
CANCEL: "requestingCancel",
RETRY_BUILD: [
{
target: "requestingStart",
cond: "lastBuildWasStarting",
actions: ["enableDebugMode"],
},
{
target: "requestingStop",
cond: "lastBuildWasStopping",
actions: ["enableDebugMode"],
},
{
target: "requestingDelete",
cond: "lastBuildWasDeleting",
actions: ["enableDebugMode"],
},
],
},
},
askingDelete: {
@@ -330,7 +354,7 @@ export const workspaceMachine = createMachine(
id: "startWorkspace",
onDone: [
{
actions: ["assignBuild"],
actions: ["assignBuild", "disableDebugMode"],
target: "idle",
},
],
@@ -349,7 +373,7 @@ export const workspaceMachine = createMachine(
id: "stopWorkspace",
onDone: [
{
actions: ["assignBuild"],
actions: ["assignBuild", "disableDebugMode"],
target: "idle",
},
],
@@ -368,7 +392,7 @@ export const workspaceMachine = createMachine(
id: "deleteWorkspace",
onDone: [
{
actions: ["assignBuild"],
actions: ["assignBuild", "disableDebugMode"],
target: "idle",
},
],
@@ -489,7 +513,7 @@ export const workspaceMachine = createMachine(
schedule: {
invoke: {
id: "scheduleBannerMachine",
src: workspaceScheduleBannerMachine,
src: "scheduleBannerMachine",
data: {
workspace: (context: WorkspaceContext) => context.workspace,
},
@@ -498,11 +522,7 @@ export const workspaceMachine = createMachine(
},
},
error: {
on: {
GET_WORKSPACE: {
target: "gettingWorkspace",
},
},
type: "final",
},
},
},
@@ -646,22 +666,30 @@ export const workspaceMachine = createMachine(
return data.parameters
},
}),
// Debug mode when build fails
enableDebugMode: assign({ createBuildLogLevel: (_) => "debug" as const }),
disableDebugMode: assign({ createBuildLogLevel: (_) => undefined }),
},
guards: {
moreBuildsAvailable,
isMissingBuildParameterError: (_, { data }) => {
return data instanceof API.MissingBuildParameters
},
lastBuildWasStarting: ({ workspace }) => {
return workspace?.latest_build.transition === "start"
},
lastBuildWasStopping: ({ workspace }) => {
return workspace?.latest_build.transition === "stop"
},
lastBuildWasDeleting: ({ workspace }) => {
return workspace?.latest_build.transition === "delete"
},
},
services: {
getWorkspace: async (_, event) => {
return await API.getWorkspaceByOwnerAndName(
event.username,
event.workspaceName,
{
include_deleted: true,
},
)
getWorkspace: async ({ username, workspaceName }) => {
return await API.getWorkspaceByOwnerAndName(username, workspaceName, {
include_deleted: true,
})
},
getTemplate: async (context) => {
if (context.workspace) {
@@ -685,6 +713,7 @@ export const workspaceMachine = createMachine(
const startWorkspacePromise = await API.startWorkspace(
context.workspace.id,
context.workspace.latest_build.template_version_id,
context.createBuildLogLevel,
)
send({ type: "REFRESH_TIMELINE" })
return startWorkspacePromise
@@ -696,6 +725,7 @@ export const workspaceMachine = createMachine(
if (context.workspace) {
const stopWorkspacePromise = await API.stopWorkspace(
context.workspace.id,
context.createBuildLogLevel,
)
send({ type: "REFRESH_TIMELINE" })
return stopWorkspacePromise
@@ -707,6 +737,7 @@ export const workspaceMachine = createMachine(
if (context.workspace) {
const deleteWorkspacePromise = await API.deleteWorkspace(
context.workspace.id,
context.createBuildLogLevel,
)
send({ type: "REFRESH_TIMELINE" })
return deleteWorkspacePromise
@@ -765,18 +796,21 @@ export const workspaceMachine = createMachine(
throw Error("Cannot get builds without id")
}
},
checkPermissions: async (context) => {
if (context.workspace) {
return await API.checkAuthorization({
checks: permissionsToCheck(context.workspace),
})
} else {
throw Error("Cannot check permissions workspace id")
checkPermissions: async ({ workspace, template }) => {
if (!workspace) {
throw new Error("Workspace is not set")
}
if (!template) {
throw new Error("Template is not set")
}
return await API.checkAuthorization({
checks: permissionsToCheck(workspace, template),
})
},
getApplicationsHost: async () => {
return API.getApplicationsHost()
},
scheduleBannerMachine: workspaceScheduleBannerMachine,
getSSHPrefix: async () => {
return API.getDeploymentSSHConfig()
},