Handle errors

This commit is contained in:
Presley Pizzo
2022-10-14 19:57:29 +00:00
parent bdb061404a
commit 86caa80263
5 changed files with 269 additions and 233 deletions
+1 -1
View File
@@ -288,7 +288,7 @@ export const getWorkspaces = async (
// TODO change types
export const getWorkspacesCount = async (
options: TypesGen.AuditLogCountRequest = {}
options: TypesGen.AuditLogCountRequest = {},
): Promise<TypesGen.AuditLogCountResponse> => {
const searchParams = new URLSearchParams()
if (options.q) {
@@ -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<AlertBannerProps> = ({
severity,
@@ -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={() => {
@@ -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<
</PageHeaderSubtitle>
</PageHeader>
<Maybe condition={getWorkspacesError !== undefined}>
<AlertBanner
error={getWorkspacesError}
severity={
workspaceRefs !== undefined && workspaceRefs.length > 0
? "warning"
: "error"
}
/>
</Maybe>
<Maybe condition={getCountError !== undefined}>
<AlertBanner error={getCountError} severity="warning" />
</Maybe>
<SearchBarWithFilter
filter={filter}
onFilter={onFilter}
@@ -206,6 +206,7 @@ interface WorkspacesContext {
workspaceRefs?: WorkspaceItemMachineRef[]
filter: string
getWorkspacesError?: Error | unknown
getCountError?: Error | unknown
page: number
count?: number
limit: number
@@ -219,229 +220,234 @@ type WorkspacesEvent =
| { type: "GO_TO_PAGE"; page: number }
export const workspacesMachine =
/** @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[]
}
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",
},
},
},
},
{
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
})
},
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 }),
},
},
)