From 86caa8026308d0505d23371f47f0d90b6f3018af Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Fri, 14 Oct 2022 19:57:29 +0000 Subject: [PATCH] Handle errors --- site/src/api/api.ts | 2 +- .../components/AlertBanner/AlertBanner.tsx | 12 +- .../pages/WorkspacesPage/WorkspacesPage.tsx | 16 +- .../WorkspacesPage/WorkspacesPageView.tsx | 20 + .../workspaces/workspacesXService.ts | 452 +++++++++--------- 5 files changed, 269 insertions(+), 233 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 54a31b7eaa..cf7e8f30bf 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -288,7 +288,7 @@ export const getWorkspaces = async ( // TODO change types export const getWorkspacesCount = async ( - options: TypesGen.AuditLogCountRequest = {} + options: TypesGen.AuditLogCountRequest = {}, ): Promise => { const searchParams = new URLSearchParams() if (options.q) { diff --git a/site/src/components/AlertBanner/AlertBanner.tsx b/site/src/components/AlertBanner/AlertBanner.tsx index 9c85781190..1f79f2e8d1 100644 --- a/site/src/components/AlertBanner/AlertBanner.tsx +++ b/site/src/components/AlertBanner/AlertBanner.tsx @@ -11,12 +11,12 @@ import { severityConstants } from "./severityConstants" import { AlertBannerCtas } from "./AlertBannerCtas" /** - * severity: the level of alert severity (see ./severityTypes.ts) - * text: default text to be displayed to the user; useful for warnings or as a fallback error message - * error: should be passed in if the severity is 'Error'; warnings can use 'text' instead - * actions: an array of CTAs passed in by the consumer - * dismissible: determines whether or not the banner should have a `Dismiss` CTA - * retry: a handler to retry the action that spawned the error + * @param severity: the level of alert severity (see ./severityTypes.ts) + * @param text: default text to be displayed to the user; useful for warnings or as a fallback error message + * @param error: should be passed in if the severity is 'Error'; warnings can use 'text' instead + * @param actions: an array of CTAs passed in by the consumer + * @param dismissible: determines whether or not the banner should have a `Dismiss` CTA + * @param retry: a handler to retry the action that spawned the error */ export const AlertBanner: FC = ({ severity, diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index fa64cbb804..d9f53b8d49 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,13 +1,14 @@ import { useMachine } from "@xstate/react" import { FC } from "react" import { Helmet } from "react-helmet-async" -import { useSearchParams } from "react-router-dom" +import { useNavigate, useSearchParams } from "react-router-dom" import { workspaceFilterQuery } from "util/filters" import { pageTitle } from "util/page" import { workspacesMachine } from "xServices/workspaces/workspacesXService" import { WorkspacesPageView } from "./WorkspacesPageView" const WorkspacesPage: FC = () => { + const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() const filter = searchParams.get("filter") ?? workspaceFilterQuery.me const currentPage = searchParams.get("page") @@ -16,7 +17,7 @@ const WorkspacesPage: FC = () => { const [workspacesState, send] = useMachine(workspacesMachine, { context: { page: currentPage, - limit: 25, + limit: 2, //TODO filter, }, actions: { @@ -28,7 +29,14 @@ const WorkspacesPage: FC = () => { }, }) - const { workspaceRefs, count, page, limit } = workspacesState.context + const { + workspaceRefs, + count, + page, + limit, + getWorkspacesError, + getCountError, + } = workspacesState.context return ( <> @@ -41,6 +49,8 @@ const WorkspacesPage: FC = () => { isLoading={!workspaceRefs} workspaceRefs={workspaceRefs} count={count} + getWorkspacesError={getWorkspacesError} + getCountError={getCountError} page={page} limit={limit} onNext={() => { diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 42a28adb7f..a016e1fd20 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -1,4 +1,5 @@ import Link from "@material-ui/core/Link" +import { AlertBanner } from "components/AlertBanner/AlertBanner" import { Maybe } from "components/Conditionals/Maybe" import { PaginationWidget } from "components/PaginationWidget/PaginationWidget" import { FC } from "react" @@ -29,6 +30,8 @@ export interface WorkspacesPageViewProps { isLoading?: boolean workspaceRefs?: WorkspaceItemMachineRef[] count?: number + getWorkspacesError: Error | unknown + getCountError: Error | unknown page: number limit: number filter?: string @@ -44,6 +47,8 @@ export const WorkspacesPageView: FC< isLoading, workspaceRefs, count, + getWorkspacesError, + getCountError, page, limit, filter, @@ -80,6 +85,21 @@ export const WorkspacesPageView: FC< + + 0 + ? "warning" + : "error" + } + /> + + + + + + !context.workspaceRefs, - }, - actions: { - assignWorkspaceRefs: assign({ - workspaceRefs: (_, event) => - event.data.map((data) => { - return spawn(workspaceItemMachine.withContext({ data }), data.id) - }), - }), - assignFilter: assign({ - filter: (context, event) => event.query ?? context.filter, - }), - assignGetWorkspacesError: assign({ - getWorkspacesError: (_, event) => event.data, - }), - clearGetWorkspacesError: (context) => - assign({ ...context, getWorkspacesError: undefined }), - triggerUpdateVersion: (context, event) => { - const workspaceRef = context.workspaceRefs?.find( - (ref) => ref.id === event.workspaceId, - ) - - if (!workspaceRef) { - throw new Error(`No workspace ref found for ${event.workspaceId}.`) - } - - workspaceRef.send("UPDATE_VERSION") - }, - assignUpdatedWorkspaceRefs: assign({ - workspaceRefs: (_, event) => { - const newWorkspaceRefs = event.data.newWorkspaces.map((workspace) => - spawn( - workspaceItemMachine.withContext({ data: workspace }), - workspace.id, - ), - ) - return event.data.refsToKeep.concat(newWorkspaceRefs) - }, - }), - assignNextPage: assign({ - page: (context) => context.page + 1, - }), - assignPreviousPage: assign({ - page: (context) => context.page - 1, - }), - assignPage: assign({ - page: (_, event) => event.page, - }), - assignCount: assign({ - count: (_, event) => event.data.count - }) - }, - services: { - getWorkspaces: (context) => - API.getWorkspaces(queryToFilter(context.filter)), - updateWorkspaceRefs: (context, event) => { - const refsToKeep: WorkspaceItemMachineRef[] = [] - context.workspaceRefs?.forEach((ref) => { - const matchingWorkspace = event.data.find( - (workspace) => ref.id === workspace.id, - ) - if (matchingWorkspace) { - // if a workspace machine reference describes a workspace that has not been deleted, - // update its data and mark it as a refToKeep - ref.send({ type: "UPDATE_DATA", data: matchingWorkspace }) - refsToKeep.push(ref) - } else { - // if it describes a workspace that has been deleted, stop the machine - ref.stop && ref.stop() + /** @xstate-layout N4IgpgJg5mDOIC5QHcD2AnA1rADgQwGM4BlAFz1LADpk8BLUgFVQCUwAzdOACwHUNs+IrADEAD1jlKVPO0roAFAFYADGoCUItFlyESU6rQbM2nHvx1C4AbRUBdRKBypYDOqgB2jkGMQBGAGYANgCqFQAmAE4-cIAOOPDwoIB2ZIAaEABPRHCVKljAv2S-EMiClSUAFliAXxqM7UE9WDIKanYwUgJuOg8oKgJUAFcPUioYUlJeqABhYdGRCE9qXoA3VExqCYsm4TmR0lsHJBBnVynPb18ESsq8uOLk2KCVSqVY4qUM7IQAyqoApElOFKskgpEUn5avUQI1dMJWtIOl0en0BvMxhMpn19gswOh0BgqDgADYUdgYAC2406O3hcFxh3s3jObkuJ2ufyUVEiAXi4QCySUAWBARF3xyaiowuqZShQSCiUidQaAnpLQMVGR3WmNDVVlgVCGOAgFGmdKsplESw8Kw8602RpNbQteitRxZLjZXg5iFitwByTKARUQVulQCiQlCBeeX9sWifihhXBKth+uaiPanR1aLhBppk3NGeEi2WVDWGy2tJLNmZJ1ZFx9oGub25f0jyQqSiUQvi0aUiqoiXCfnecT8kRUwTT+czmu1qP6c+EhexUFdpZtdod1dIm5sfmOTi9TauiFukWlCb5097-tD0cqQ57dw+8cCz9ntY1bS1OaXPVLGaNdi2A0t8UJdBiTJUgKXQalth-D0G1Pdxmx8fximHF5O1uVJKgFAJoxCZIqChPlIhBUMngib9wP0P9F2mMtbSoSQ-xXRikQA6YUJPc50PPBBAhSYcQWBJJcmBfsshyIpxNHLsCiUIFIyCejdm4sARAAcQAUUYAB9XgAHkWAAaWIAAFABBGZ9OIfjTjQ9kW0QABaQiwmKadkhDQjgmSSpow8gIx3Ivkg17KIwSUPxNPVLMRAAVWsgARWzGH0oyADV9JYYgAElTIAOWcxshN9BB4jIsdhReSpIgjJrwlCkU-CoYKlQqQVYniRKDWS0r9IADUYCrXIw64Sm5XsFSSHtR1BQJ2r8f5Xg+KIww+YogiUQb5zaERrJYfTcpKlKnPrATvWEwV-nBUdR0HZqRTDNaNuqZJtu+vaDphLjf0oPTTKMxgwbsgzJsEtzMIQd4gi6ypRNBO4BWeT6wm+37dtmuoYQ8VAIDgbwgazGh6CYVgOC4WA+B-T1Yem-wxUe3sij6uIOujUSeSCKF-MFXlAmBQ6EQXXi0UGA5QJxDEmbu6q-mHVT+oTEIYkIkK5N+K9ARUeIIlHNQRUqcXtP-FFdRl0YqG3RWz2qkIkdeP5XmKEEFujUckZ+0dnj5DGewCC3geza3pYV1DmeEjzAmRlQkz8acgg+ebQqqKhGqFCNIkiIUkmhVUGPDq3c2XH8nVNdcDytR2qvcmrIx5TmhUBJIk6+XXbliLrB369SDZTgGS60svmLzKusTA8eG7hzl-l7YoqKIxUShKJ83n7tOnnC7bwkHMOKcnyvS-t5Z55ZhBin+ajfIxsUIWjfy5pSXlFVDAfQ8Bn8T6ls+c8Y5KybvHUIhE4gqFSAqPqfIdY-HejvWIlFqh8gesfSWkcoBXzjgLROydU7pzBKFdaoQc6XjBOGEUGC2g4OqvHFQV5gpJyTIQoUxDdZeVeJFD4zUIypGKD-OoQA */ + createMachine( + { + tsTypes: {} as import("./workspacesXService.typegen").Typegen1, + schema: { + context: {} as WorkspacesContext, + events: {} as WorkspacesEvent, + services: {} as { + getWorkspaces: { + data: TypesGen.Workspace[] } - }) - - const newWorkspaces = event.data.filter( - (workspace) => - !context.workspaceRefs?.find((ref) => ref.id === workspace.id), - ) - - return Promise.resolve({ - refsToKeep, - newWorkspaces, - }) + getWorkspacesCount: { + data: { count: number } + } + updateWorkspaceRefs: { + data: { + refsToKeep: WorkspaceItemMachineRef[] + newWorkspaces: TypesGen.Workspace[] + } + } + }, + }, + predictableActionArguments: true, + id: "workspacesState", + on: { + GET_WORKSPACES: { + target: ".fetching", + actions: "assignFilter", + }, + UPDATE_VERSION: { + actions: "triggerUpdateVersion", + }, + NEXT: { + target: ".fetching", + actions: ["assignNextPage", "onPageChange"], + }, + PREVIOUS: { + target: ".fetching", + actions: ["assignPreviousPage", "onPageChange"], + }, + GO_TO_PAGE: { + target: ".fetching", + actions: ["assignPage", "onPageChange"], + }, + }, + initial: "fetching", + states: { + waitToRefreshWorkspaces: { + after: { + "5000": { + target: "#workspacesState.fetching", + actions: [], + internal: false, + }, + }, + }, + fetching: { + type: "parallel", + states: { + count: { + initial: "gettingCount", + states: { + gettingCount: { + entry: "clearGetCountError", + invoke: { + src: "getWorkspacesCount", + id: "getWorkspacesCount", + onDone: [ + { + target: "done", + actions: "assignCount", + }, + ], + onError: [ + { + target: "done", + actions: "assignGetCountError", + }, + ], + }, + }, + done: { + type: "final", + }, + }, + }, + workspaces: { + initial: "gettingWorkspaces", + states: { + updatingWorkspaceRefs: { + invoke: { + src: "updateWorkspaceRefs", + id: "updateWorkspaceRefs", + onDone: [ + { + target: "done", + actions: "assignUpdatedWorkspaceRefs", + }, + ], + }, + }, + gettingWorkspaces: { + entry: "clearGetWorkspacesError", + invoke: { + src: "getWorkspaces", + id: "getWorkspaces", + onDone: [ + { + target: "done", + cond: "isEmpty", + actions: "assignWorkspaceRefs", + }, + { + target: "updatingWorkspaceRefs", + }, + ], + onError: [ + { + target: "done", + actions: "assignGetWorkspacesError", + }, + ], + }, + }, + done: { + type: "final", + }, + }, + }, + }, + onDone: { + target: "waitToRefreshWorkspaces", + }, + }, }, - getWorkspacesCount: (context) => ( - API.getWorkspacesCount({ q: context.filter }) - ) }, - }, -) + { + guards: { + isEmpty: (context) => !context.workspaceRefs, + }, + actions: { + assignWorkspaceRefs: assign({ + workspaceRefs: (_, event) => + event.data.map((data) => { + return spawn(workspaceItemMachine.withContext({ data }), data.id) + }), + }), + assignFilter: assign({ + filter: (context, event) => event.query ?? context.filter, + }), + assignGetWorkspacesError: assign({ + getWorkspacesError: (_, event) => event.data, + }), + clearGetWorkspacesError: (context) => + assign({ ...context, getWorkspacesError: undefined }), + triggerUpdateVersion: (context, event) => { + const workspaceRef = context.workspaceRefs?.find( + (ref) => ref.id === event.workspaceId, + ) + + if (!workspaceRef) { + throw new Error(`No workspace ref found for ${event.workspaceId}.`) + } + + workspaceRef.send("UPDATE_VERSION") + }, + assignUpdatedWorkspaceRefs: assign({ + workspaceRefs: (_, event) => { + const newWorkspaceRefs = event.data.newWorkspaces.map((workspace) => + spawn( + workspaceItemMachine.withContext({ data: workspace }), + workspace.id, + ), + ) + return event.data.refsToKeep.concat(newWorkspaceRefs) + }, + }), + assignNextPage: assign({ + page: (context) => context.page + 1, + }), + assignPreviousPage: assign({ + page: (context) => context.page - 1, + }), + assignPage: assign({ + page: (_, event) => event.page, + }), + assignCount: assign({ + count: (_, event) => event.data.count, + }), + assignGetCountError: assign({ + getCountError: (_, event) => event.data, + }), + clearGetCountError: assign({ + getCountError: (_) => undefined, + }), + }, + services: { + getWorkspaces: (context) => + API.getWorkspaces(queryToFilter(context.filter)), + updateWorkspaceRefs: (context, event) => { + const refsToKeep: WorkspaceItemMachineRef[] = [] + context.workspaceRefs?.forEach((ref) => { + const matchingWorkspace = event.data.find( + (workspace) => ref.id === workspace.id, + ) + if (matchingWorkspace) { + // if a workspace machine reference describes a workspace that has not been deleted, + // update its data and mark it as a refToKeep + ref.send({ type: "UPDATE_DATA", data: matchingWorkspace }) + refsToKeep.push(ref) + } else { + // if it describes a workspace that has been deleted, stop the machine + ref.stop && ref.stop() + } + }) + + const newWorkspaces = event.data.filter( + (workspace) => + !context.workspaceRefs?.find((ref) => ref.id === workspace.id), + ) + + return Promise.resolve({ + refsToKeep, + newWorkspaces, + }) + }, + getWorkspacesCount: (context) => + API.getWorkspacesCount({ q: context.filter }), + }, + }, + )