mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: handle workspace errors (#3341)
This commit is contained in:
@@ -6,6 +6,7 @@ import TableContainer from "@material-ui/core/TableContainer"
|
||||
import TableHead from "@material-ui/core/TableHead"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import useTheme from "@material-ui/styles/useTheme"
|
||||
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
|
||||
import { FC } from "react"
|
||||
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
|
||||
import { AvatarData } from "../../components/AvatarData/AvatarData"
|
||||
@@ -28,7 +29,7 @@ const Language = {
|
||||
|
||||
interface ResourcesProps {
|
||||
resources?: WorkspaceResource[]
|
||||
getResourcesError?: Error
|
||||
getResourcesError?: Error | unknown
|
||||
workspace: Workspace
|
||||
canUpdateWorkspace: boolean
|
||||
}
|
||||
@@ -45,7 +46,7 @@ export const Resources: FC<ResourcesProps> = ({
|
||||
return (
|
||||
<div aria-label={Language.resources} className={styles.wrapper}>
|
||||
{getResourcesError ? (
|
||||
{ getResourcesError }
|
||||
<ErrorSummary error={getResourcesError} />
|
||||
) : (
|
||||
<TableContainer className={styles.tableContainer}>
|
||||
<Table>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import * as Mocks from "../../testHelpers/entities"
|
||||
import { Workspace, WorkspaceProps } from "./Workspace"
|
||||
import { Workspace, WorkspaceErrors, WorkspaceProps } from "./Workspace"
|
||||
|
||||
export default {
|
||||
title: "components/Workspace",
|
||||
@@ -31,6 +31,7 @@ Started.args = {
|
||||
resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2],
|
||||
builds: [Mocks.MockWorkspaceBuild],
|
||||
canUpdateWorkspace: true,
|
||||
workspaceErrors: {},
|
||||
}
|
||||
|
||||
export const WithoutUpdateAccess = Template.bind({})
|
||||
@@ -71,6 +72,11 @@ Error.args = {
|
||||
transition: "start",
|
||||
},
|
||||
},
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.BUILD_ERROR]: Mocks.makeMockApiError({
|
||||
message: "A workspace build is already active.",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const Deleting = Template.bind({})
|
||||
@@ -102,3 +108,33 @@ Outdated.args = {
|
||||
...Started.args,
|
||||
workspace: Mocks.MockOutdatedWorkspace,
|
||||
}
|
||||
|
||||
export const GetBuildsError = Template.bind({})
|
||||
GetBuildsError.args = {
|
||||
...Started.args,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.GET_BUILDS_ERROR]: Mocks.makeMockApiError({
|
||||
message: "There is a problem fetching builds.",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const GetResourcesError = Template.bind({})
|
||||
GetResourcesError.args = {
|
||||
...Started.args,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.GET_RESOURCES_ERROR]: Mocks.makeMockApiError({
|
||||
message: "There is a problem fetching workspace resources.",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export const CancellationError = Template.bind({})
|
||||
CancellationError.args = {
|
||||
...Error.args,
|
||||
workspaceErrors: {
|
||||
[WorkspaceErrors.CANCELLATION_ERROR]: Mocks.makeMockApiError({
|
||||
message: "Job could not be canceled.",
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
|
||||
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
|
||||
import { FC } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
@@ -15,6 +16,13 @@ import { WorkspaceScheduleButton } from "../WorkspaceScheduleButton/WorkspaceSch
|
||||
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
|
||||
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
|
||||
|
||||
export enum WorkspaceErrors {
|
||||
GET_RESOURCES_ERROR = "getResourcesError",
|
||||
GET_BUILDS_ERROR = "getBuildsError",
|
||||
BUILD_ERROR = "buildError",
|
||||
CANCELLATION_ERROR = "cancellationError",
|
||||
}
|
||||
|
||||
export interface WorkspaceProps {
|
||||
bannerProps: {
|
||||
isLoading?: boolean
|
||||
@@ -31,9 +39,9 @@ export interface WorkspaceProps {
|
||||
handleCancel: () => void
|
||||
workspace: TypesGen.Workspace
|
||||
resources?: TypesGen.WorkspaceResource[]
|
||||
getResourcesError?: Error
|
||||
builds?: TypesGen.WorkspaceBuild[]
|
||||
canUpdateWorkspace: boolean
|
||||
workspaceErrors: Partial<Record<WorkspaceErrors, Error | unknown>>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,15 +57,23 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
handleCancel,
|
||||
workspace,
|
||||
resources,
|
||||
getResourcesError,
|
||||
builds,
|
||||
canUpdateWorkspace,
|
||||
workspaceErrors,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<Stack spacing={1}>
|
||||
{workspaceErrors[WorkspaceErrors.BUILD_ERROR] && (
|
||||
<ErrorSummary error={workspaceErrors[WorkspaceErrors.BUILD_ERROR]} dismissible />
|
||||
)}
|
||||
{workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR] && (
|
||||
<ErrorSummary error={workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR]} dismissible />
|
||||
)}
|
||||
</Stack>
|
||||
<PageHeader
|
||||
actions={
|
||||
<Stack direction="row" spacing={1} className={styles.actions}>
|
||||
@@ -101,14 +117,18 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
{!!resources && !!resources.length && (
|
||||
<Resources
|
||||
resources={resources}
|
||||
getResourcesError={getResourcesError}
|
||||
getResourcesError={workspaceErrors[WorkspaceErrors.GET_RESOURCES_ERROR]}
|
||||
workspace={workspace}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
/>
|
||||
)}
|
||||
|
||||
<WorkspaceSection title="Logs" contentsProps={{ className: styles.timelineContents }}>
|
||||
<BuildsTable builds={builds} className={styles.timelineTable} />
|
||||
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
|
||||
<ErrorSummary error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]} />
|
||||
) : (
|
||||
<BuildsTable builds={builds} className={styles.timelineTable} />
|
||||
)}
|
||||
</WorkspaceSection>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { useMachine, useSelector } from "@xstate/react"
|
||||
import dayjs from "dayjs"
|
||||
import minMax from "dayjs/plugin/minMax"
|
||||
@@ -7,7 +8,7 @@ import { useParams } from "react-router-dom"
|
||||
import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"
|
||||
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
|
||||
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
|
||||
import { Workspace } from "../../components/Workspace/Workspace"
|
||||
import { Workspace, WorkspaceErrors } from "../../components/Workspace/Workspace"
|
||||
import { firstOrItem } from "../../util/array"
|
||||
import { pageTitle } from "../../util/page"
|
||||
import { getFaviconByStatus } from "../../util/workspace"
|
||||
@@ -31,13 +32,25 @@ export const WorkspacePage: React.FC = () => {
|
||||
userId: me?.id,
|
||||
},
|
||||
})
|
||||
const { workspace, resources, getWorkspaceError, getResourcesError, builds, permissions } =
|
||||
workspaceState.context
|
||||
const {
|
||||
workspace,
|
||||
getWorkspaceError,
|
||||
resources,
|
||||
getResourcesError,
|
||||
builds,
|
||||
getBuildsError,
|
||||
permissions,
|
||||
checkPermissionsError,
|
||||
buildError,
|
||||
cancellationError,
|
||||
} = workspaceState.context
|
||||
|
||||
const canUpdateWorkspace = !!permissions?.updateWorkspace
|
||||
|
||||
const [bannerState, bannerSend] = useMachine(workspaceScheduleBannerMachine)
|
||||
|
||||
const styles = useStyles()
|
||||
|
||||
/**
|
||||
* Get workspace, template, and organization on mount and whenever workspaceId changes.
|
||||
* workspaceSend should not change.
|
||||
@@ -47,7 +60,12 @@ export const WorkspacePage: React.FC = () => {
|
||||
}, [username, workspaceName, workspaceSend])
|
||||
|
||||
if (workspaceState.matches("error")) {
|
||||
return <ErrorSummary error={getWorkspaceError} />
|
||||
return (
|
||||
<div className={styles.error}>
|
||||
{getWorkspaceError && <ErrorSummary error={getWorkspaceError} />}
|
||||
{checkPermissionsError && <ErrorSummary error={checkPermissionsError} />}
|
||||
</div>
|
||||
)
|
||||
} else if (!workspace) {
|
||||
return <FullScreenLoader />
|
||||
} else {
|
||||
@@ -100,9 +118,14 @@ export const WorkspacePage: React.FC = () => {
|
||||
handleUpdate={() => workspaceSend("UPDATE")}
|
||||
handleCancel={() => workspaceSend("CANCEL")}
|
||||
resources={resources}
|
||||
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
|
||||
builds={builds}
|
||||
canUpdateWorkspace={canUpdateWorkspace}
|
||||
workspaceErrors={{
|
||||
[WorkspaceErrors.GET_RESOURCES_ERROR]: getResourcesError,
|
||||
[WorkspaceErrors.GET_BUILDS_ERROR]: getBuildsError,
|
||||
[WorkspaceErrors.BUILD_ERROR]: buildError,
|
||||
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
|
||||
}}
|
||||
/>
|
||||
<DeleteWorkspaceDialog
|
||||
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
||||
@@ -121,3 +144,9 @@ export const boundedDeadline = (newDeadline: dayjs.Dayjs, now: dayjs.Dayjs): day
|
||||
const maxDeadline = now.add(24, "hours")
|
||||
return dayjs.min(dayjs.max(minDeadline, newDeadline), maxDeadline)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
error: {
|
||||
margin: theme.spacing(2),
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -3,7 +3,7 @@ import { pure } from "xstate/lib/actions"
|
||||
import * as API from "../../api/api"
|
||||
import * as Types from "../../api/types"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { displayError } from "../../components/GlobalSnackbar/utils"
|
||||
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
|
||||
|
||||
const latestBuild = (builds: TypesGen.WorkspaceBuild[]) => {
|
||||
// Cloning builds to not change the origin object with the sort()
|
||||
@@ -35,7 +35,8 @@ export interface WorkspaceContext {
|
||||
builds?: TypesGen.WorkspaceBuild[]
|
||||
getBuildsError?: Error | unknown
|
||||
loadMoreBuildsError?: Error | unknown
|
||||
cancellationMessage: string
|
||||
cancellationMessage?: Types.Message
|
||||
cancellationError?: Error | unknown
|
||||
// permissions
|
||||
permissions?: Permissions
|
||||
checkPermissionsError?: Error | unknown
|
||||
@@ -97,6 +98,9 @@ export const workspaceMachine = createMachine(
|
||||
stopWorkspace: {
|
||||
data: TypesGen.WorkspaceBuild
|
||||
}
|
||||
deleteWorkspace: {
|
||||
data: TypesGen.WorkspaceBuild
|
||||
}
|
||||
cancelWorkspace: {
|
||||
data: Types.Message
|
||||
}
|
||||
@@ -213,7 +217,7 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: ["assignBuildError", "displayBuildError"],
|
||||
actions: ["assignBuildError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -228,7 +232,7 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: ["assignBuildError", "displayBuildError"],
|
||||
actions: ["assignBuildError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -243,22 +247,26 @@ export const workspaceMachine = createMachine(
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: ["assignBuildError", "displayBuildError"],
|
||||
actions: ["assignBuildError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
requestingCancel: {
|
||||
entry: "clearCancellationMessage",
|
||||
entry: ["clearCancellationMessage", "clearCancellationError"],
|
||||
invoke: {
|
||||
id: "cancelWorkspace",
|
||||
src: "cancelWorkspace",
|
||||
onDone: {
|
||||
target: "idle",
|
||||
actions: ["assignCancellationMessage", "refreshTimeline"],
|
||||
actions: [
|
||||
"assignCancellationMessage",
|
||||
"displayCancellationMessage",
|
||||
"refreshTimeline",
|
||||
],
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: ["assignCancellationMessage", "displayCancellationError"],
|
||||
actions: ["assignCancellationError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -387,63 +395,57 @@ export const workspaceMachine = createMachine(
|
||||
clearGetPermissionsError: assign({
|
||||
checkPermissionsError: (_) => undefined,
|
||||
}),
|
||||
assignBuild: (_, event) =>
|
||||
assign({
|
||||
build: event.data,
|
||||
}),
|
||||
assignBuildError: (_, event) =>
|
||||
assign({
|
||||
buildError: event.data,
|
||||
}),
|
||||
displayBuildError: () => {
|
||||
displayError(Language.buildError)
|
||||
assignBuild: assign({
|
||||
build: (_, event) => event.data,
|
||||
}),
|
||||
assignBuildError: assign({
|
||||
buildError: (_, event) => event.data,
|
||||
}),
|
||||
clearBuildError: assign({
|
||||
buildError: (_) => undefined,
|
||||
}),
|
||||
assignCancellationMessage: assign({
|
||||
cancellationMessage: (_, event) => event.data,
|
||||
}),
|
||||
clearCancellationMessage: assign({
|
||||
cancellationMessage: (_) => undefined,
|
||||
}),
|
||||
displayCancellationMessage: (context) => {
|
||||
if (context.cancellationMessage) {
|
||||
displaySuccess(context.cancellationMessage.message)
|
||||
}
|
||||
},
|
||||
clearBuildError: (_) =>
|
||||
assign({
|
||||
buildError: undefined,
|
||||
}),
|
||||
assignCancellationMessage: (_, event) =>
|
||||
assign({
|
||||
cancellationMessage: event.data,
|
||||
}),
|
||||
clearCancellationMessage: (_) =>
|
||||
assign({
|
||||
cancellationMessage: undefined,
|
||||
}),
|
||||
displayCancellationError: (context) => {
|
||||
displayError(context.cancellationMessage)
|
||||
},
|
||||
assignRefreshWorkspaceError: (_, event) =>
|
||||
assign({
|
||||
refreshWorkspaceError: event.data,
|
||||
}),
|
||||
clearRefreshWorkspaceError: (_) =>
|
||||
assign({
|
||||
refreshWorkspaceError: undefined,
|
||||
}),
|
||||
assignRefreshTemplateError: (_, event) =>
|
||||
assign({
|
||||
refreshTemplateError: event.data,
|
||||
}),
|
||||
assignCancellationError: assign({
|
||||
cancellationError: (_, event) => event.data,
|
||||
}),
|
||||
clearCancellationError: assign({
|
||||
cancellationError: (_) => undefined,
|
||||
}),
|
||||
assignRefreshWorkspaceError: assign({
|
||||
refreshWorkspaceError: (_, event) => event.data,
|
||||
}),
|
||||
clearRefreshWorkspaceError: assign({
|
||||
refreshWorkspaceError: (_) => undefined,
|
||||
}),
|
||||
assignRefreshTemplateError: assign({
|
||||
refreshTemplateError: (_, event) => event.data,
|
||||
}),
|
||||
displayRefreshTemplateError: () => {
|
||||
displayError(Language.refreshTemplateError)
|
||||
},
|
||||
clearRefreshTemplateError: (_) =>
|
||||
assign({
|
||||
refreshTemplateError: undefined,
|
||||
}),
|
||||
clearRefreshTemplateError: assign({
|
||||
refreshTemplateError: (_) => undefined,
|
||||
}),
|
||||
// Resources
|
||||
assignResources: assign({
|
||||
resources: (_, event) => event.data,
|
||||
}),
|
||||
assignGetResourcesError: (_, event) =>
|
||||
assign({
|
||||
getResourcesError: event.data,
|
||||
}),
|
||||
clearGetResourcesError: (_) =>
|
||||
assign({
|
||||
getResourcesError: undefined,
|
||||
}),
|
||||
assignGetResourcesError: assign({
|
||||
getResourcesError: (_, event) => event.data,
|
||||
}),
|
||||
clearGetResourcesError: assign({
|
||||
getResourcesError: (_) => undefined,
|
||||
}),
|
||||
// Timeline
|
||||
assignBuilds: assign({
|
||||
builds: (_, event) => event.data,
|
||||
|
||||
Reference in New Issue
Block a user