Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 31, 2023
16 changes: 12 additions & 4 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,15 +489,23 @@ export const postWorkspaceBuild = async (
export const startWorkspace = (
workspaceId: string,
templateVersionID: string,
debug = false,
) =>
postWorkspaceBuild(workspaceId, {
transition: "start",
template_version_id: templateVersionID,
log_level: debug ? "debug" : undefined,
})
export const stopWorkspace = (workspaceId: string, debug = false) =>
postWorkspaceBuild(workspaceId, {
transition: "stop",
log_level: debug ? "debug" : undefined,
})
export const deleteWorkspace = (workspaceId: string, debug = false) =>
postWorkspaceBuild(workspaceId, {
transition: "delete",
log_level: debug ? "debug" : undefined,
})
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"],
Expand Down
17 changes: 13 additions & 4 deletions site/src/components/AlertBanner/AlertBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 && (
Expand Down Expand Up @@ -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`,
},

Expand All @@ -108,4 +113,8 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
marginRight: `${theme.spacing(1)}px`,
},
}),

fullWidth: {
width: "100%",
},
}))
10 changes: 3 additions & 7 deletions site/src/components/AlertBanner/severityConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,20 @@ export const severityConstants: Record<
color: colors.orange[7],
icon: (
<ReportProblemOutlinedIcon
fontSize="small"
style={{ color: colors.orange[7] }}
style={{ color: colors.orange[7], fontSize: 16 }}
/>
),
},
error: {
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 }} />,
},
}
4 changes: 2 additions & 2 deletions site/src/components/Logs/Logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const useStyles = makeStyles<
background: theme.palette.background.default,
},
scrollWrapper: {
width: "fit-content",
minWidth: "fit-content",
},
line: {
wordBreak: "break-all",
Expand All @@ -109,7 +109,7 @@ const useStyles = makeStyles<
},

"&.debug": {
backgroundColor: theme.palette.grey[900],
backgroundColor: theme.palette.background.paperLight,
},

"&.warn": {
Expand Down
48 changes: 48 additions & 0 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
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,
Expand Down Expand Up @@ -55,6 +58,8 @@ export interface WorkspaceProps {
applicationsHost?: string
template?: TypesGen.Template
quota_budget?: number
failedBuildLogs: TypesGen.ProvisionerJobLog[] | undefined
handleBuildRetry: () => void
}

/**
Expand All @@ -80,6 +85,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
applicationsHost,
template,
quota_budget,
failedBuildLogs,
handleBuildRetry,
}) => {
const styles = useStyles()
const navigate = useNavigate()
Expand Down Expand Up @@ -177,6 +184,38 @@ 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>

<div>
<Button
onClick={handleBuildRetry}
startIcon={<RefreshOutlined />}
size="small"
variant="outlined"
>
Try again in debug mode
</Button>
</div>
</Stack>
</AlertBanner>
<WorkspaceBuildLogs logs={failedBuildLogs} />
</Stack>
)}

{transitionStats !== undefined && (
<WorkspaceBuildProgress
workspace={workspace}
Expand Down Expand Up @@ -252,5 +291,14 @@ export const useStyles = makeStyles((theme) => {
logs: {
border: `1px solid ${theme.palette.divider}`,
},

errorDetails: {
color: theme.palette.text.secondary,
fontSize: 12,
},

fullWidth: {
width: "100%",
},
}
})
6 changes: 6 additions & 0 deletions site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -113,6 +114,7 @@ const useStyles = makeStyles<
"&:first-child": {
borderTopLeftRadius: theme.shape.borderRadius,
borderTopRightRadius: theme.shape.borderRadius,
borderTop: 0,
},

"&:last-child": {
Expand All @@ -121,6 +123,10 @@ const useStyles = makeStyles<
borderBottomLeftRadius: theme.shape.borderRadius,
borderBottomRightRadius: theme.shape.borderRadius,
},

"& + $header": {
borderTop: 0,
},
},

duration: {
Expand Down
56 changes: 34 additions & 22 deletions site/src/pages/WorkspacePage/WorkspacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,56 @@
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"

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,
getTemplateWarning,
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 (
<ChooseOne>
Expand Down Expand Up @@ -76,6 +87,7 @@ export const WorkspacePage: FC = () => {
workspaceState={workspaceState}
quotaState={quotaState}
workspaceSend={workspaceSend}
failedBuildLogs={failedBuildLogs.data}
/>
</Cond>
<Cond>
Expand Down
5 changes: 5 additions & 0 deletions site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -85,6 +88,7 @@ export const WorkspaceReadyPage = ({
</Helmet>

<Workspace
failedBuildLogs={failedBuildLogs}
scheduleProps={{
onDeadlineMinus: (hours: number) => {
bannerSend({
Expand Down Expand Up @@ -112,6 +116,7 @@ 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}
Expand Down
18 changes: 4 additions & 14 deletions site/src/xServices/quotas/quotasXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,27 @@ 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",
predictableActionArguments: true,
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: {
Expand Down Expand Up @@ -67,7 +57,7 @@ export const quotaMachine = createMachine(
}),
},
services: {
getQuota: (context, event) => API.getWorkspaceQuota(event.username),
getQuota: ({ username }) => API.getWorkspaceQuota(username),
},
},
)
Loading