diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 209c4f322ccd7..190ec4baae23b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -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"], diff --git a/site/src/components/AlertBanner/AlertBanner.tsx b/site/src/components/AlertBanner/AlertBanner.tsx index 05cd96a328d64..937d0d5b6a5d1 100644 --- a/site/src/components/AlertBanner/AlertBanner.tsx +++ b/site/src/components/AlertBanner/AlertBanner.tsx @@ -60,9 +60,14 @@ export const AlertBanner: FC> = ({ spacing={0} justifyContent="space-between" > - + {severityConstants[severity].icon} - + {children} {alertMessage} {detail && ( @@ -94,11 +99,11 @@ const useStyles = makeStyles((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) => ({ marginRight: `${theme.spacing(1)}px`, }, }), + + fullWidth: { + width: "100%", + }, })) diff --git a/site/src/components/AlertBanner/severityConstants.tsx b/site/src/components/AlertBanner/severityConstants.tsx index fcc5e3cc0c89f..bb8dfc21e04aa 100644 --- a/site/src/components/AlertBanner/severityConstants.tsx +++ b/site/src/components/AlertBanner/severityConstants.tsx @@ -13,8 +13,7 @@ export const severityConstants: Record< color: colors.orange[7], icon: ( ), }, @@ -22,15 +21,12 @@ export const severityConstants: Record< color: colors.red[7], icon: ( ), }, info: { color: colors.blue[7], - icon: ( - - ), + icon: , }, } diff --git a/site/src/components/Logs/Logs.tsx b/site/src/components/Logs/Logs.tsx index c85c47e5d5cde..e3d82488a0a5d 100644 --- a/site/src/components/Logs/Logs.tsx +++ b/site/src/components/Logs/Logs.tsx @@ -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": { diff --git a/site/src/components/Workspace/Workspace.stories.tsx b/site/src/components/Workspace/Workspace.stories.tsx index 9e97424f016be..a6fb225a20a7d 100644 --- a/site/src/components/Workspace/Workspace.stories.tsx +++ b/site/src/components/Workspace/Workspace.stories.tsx @@ -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: "", + }, + ] +} diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 126b4146a4b06..a8479fa68cae0 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -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> @@ -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> = ({ resources, builds, canUpdateWorkspace, + canUpdateTemplate, workspaceErrors, hideSSHButton, hideVSCodeDesktopButton, @@ -82,10 +90,13 @@ export const Workspace: FC> = ({ 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]) && ( > = ({ handleUpdate={handleUpdate} /> + {failedBuildLogs && ( + + + + + Workspace build failed + + {workspace.latest_build.job.error} + + + + {canUpdateTemplate && ( +
+ +
+ )} +
+
+ +
+ )} + {transitionStats !== undefined && ( { logs: { border: `1px solid ${theme.palette.divider}`, }, + + errorDetails: { + color: theme.palette.text.secondary, + fontSize: 12, + }, + + fullWidth: { + width: "100%", + }, } }) diff --git a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx index 7ba82f2b7075f..d64d54c94a1cf 100644 --- a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx +++ b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx @@ -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: { diff --git a/site/src/i18n/en/workspacePage.json b/site/src/i18n/en/workspacePage.json index b9b63d1d3d9da..fa5e9f8335021 100644 --- a/site/src/i18n/en/workspacePage.json +++ b/site/src/i18n/en/workspacePage.json @@ -28,7 +28,8 @@ "starting": "Starting...", "stopping": "Stopping...", "deleting": "Deleting...", - "settings": "Settings" + "settings": "Settings", + "retryDebugMode": "Try again in debug mode" }, "disabledButton": { "canceling": "Canceling", diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 673d12a1e2e23..2e326e1798e5a 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -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 ( { } > quotaState: StateFrom 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 = ({ { 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={{ diff --git a/site/src/xServices/quotas/quotasXService.ts b/site/src/xServices/quotas/quotasXService.ts index 84e22124f65e0..cf6784a64d917 100644 --- a/site/src/xServices/quotas/quotasXService.ts +++ b/site/src/xServices/quotas/quotasXService.ts @@ -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), }, }, ) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index eb319bc480908..1c888e2d2576a 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -52,6 +52,9 @@ const Language = { type Permissions = Record, 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() },