diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index debbf2bfe45d7..989ddda75dab6 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -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 = ({ return (
{getResourcesError ? ( - { getResourcesError } + ) : ( diff --git a/site/src/components/Workspace/Workspace.stories.tsx b/site/src/components/Workspace/Workspace.stories.tsx index c94da97054640..7a64657d4d5dd 100644 --- a/site/src/components/Workspace/Workspace.stories.tsx +++ b/site/src/components/Workspace/Workspace.stories.tsx @@ -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.", + }), + }, +} diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index de4b4eb131daf..e52099fac1d16 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -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> } /** @@ -49,15 +57,23 @@ export const Workspace: FC = ({ handleCancel, workspace, resources, - getResourcesError, builds, canUpdateWorkspace, + workspaceErrors, }) => { const styles = useStyles() const navigate = useNavigate() return ( + + {workspaceErrors[WorkspaceErrors.BUILD_ERROR] && ( + + )} + {workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR] && ( + + )} + @@ -101,14 +117,18 @@ export const Workspace: FC = ({ {!!resources && !!resources.length && ( )} - + {workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? ( + + ) : ( + + )} diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 9a87398707711..99a25f994a813 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -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 + return ( +
+ {getWorkspaceError && } + {checkPermissionsError && } +
+ ) } else if (!workspace) { return } 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, + }} /> ({ + error: { + margin: theme.spacing(2), + }, +})) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index a6db5a6d61498..acdb7b0987cbe 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -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) - }, - clearBuildError: (_) => - assign({ - buildError: undefined, - }), - assignCancellationMessage: (_, event) => - assign({ - cancellationMessage: event.data, - }), - clearCancellationMessage: (_) => - assign({ - cancellationMessage: undefined, - }), - displayCancellationError: (context) => { - displayError(context.cancellationMessage) + 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) + } }, - 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,