fix: handle workspace errors (#3341)

This commit is contained in:
Abhineet Jain
2022-08-05 10:38:07 -05:00
committed by GitHub
parent 01fe5e668e
commit e7bc01383c
5 changed files with 157 additions and 69 deletions
+3 -2
View File
@@ -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.",
}),
},
}
+24 -4
View File
@@ -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>
+34 -5
View File
@@ -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,