From b0db1dd8bc8cf756a8a9c391349c43b262c9d6b7 Mon Sep 17 00:00:00 2001 From: FoxxMD Date: Wed, 24 Jun 2026 17:18:26 +0000 Subject: [PATCH] feat(ui): Add error boundaries and better error detection for rendering --- src/client/AppNext.tsx | 5 ++-- src/client/components/ActivityTimeline.tsx | 3 ++ src/client/components/AppHeader.tsx | 3 +- src/client/components/ErrorAlert.tsx | 11 +++---- src/client/components/ErrorBoundary.tsx | 2 +- src/client/components/MSCollapsible.tsx | 3 ++ src/client/components/PlayData.tsx | 29 ++++++++++--------- .../msComponent/MSComponentDetailed.tsx | 3 ++ .../msComponent/MSComponentList.tsx | 3 +- .../msComponent/MSComponentSummary.tsx | 8 +++-- src/core/ErrorUtils.ts | 15 ++++++++++ 11 files changed, 60 insertions(+), 25 deletions(-) diff --git a/src/client/AppNext.tsx b/src/client/AppNext.tsx index 31dde983..1f6981a3 100644 --- a/src/client/AppNext.tsx +++ b/src/client/AppNext.tsx @@ -18,6 +18,7 @@ import { NAV_LINKS, SideNavItems } from './components/SideNav'; import { LogsFetchable } from './components/LogsNext'; import { SplitLayout } from './components/layouts/SplitLayout'; import { ComponentDetailedRoutable } from './components/msComponent/MSComponentDetailed'; +import { MSErrorBoundary } from './components/ErrorBoundary'; function NoMatch() { const location = useLocation(); @@ -73,11 +74,11 @@ const routesNested: RouteObject[] = [ Component: Layout, children: [ { index: true, - element: , + element: , }, { path: "components/:componentId", - element: + element: }, { path: "*", diff --git a/src/client/components/ActivityTimeline.tsx b/src/client/components/ActivityTimeline.tsx index 1d1809a8..1569847d 100644 --- a/src/client/components/ActivityTimeline.tsx +++ b/src/client/components/ActivityTimeline.tsx @@ -20,6 +20,7 @@ import { MSCollapsible } from "./MSCollapsible"; import { TimelineErrorIcon } from "./timeline/TimelineIcon"; import { Muted } from "./Typography"; import { PlayApiCommonDetailed } from "../../core/Api"; +import { MSErrorBoundary } from "./ErrorBoundary"; export interface ActivityDetailProps { @@ -103,6 +104,7 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { } return ( + @@ -241,5 +243,6 @@ export const ActivityTimeline = (props: ActivityDetailProps) => { ) : null} + ) } \ No newline at end of file diff --git a/src/client/components/AppHeader.tsx b/src/client/components/AppHeader.tsx index 90b03132..fb58d36e 100644 --- a/src/client/components/AppHeader.tsx +++ b/src/client/components/AppHeader.tsx @@ -16,6 +16,7 @@ import { useSSEContext, useSSEStatus } from "@flamefrontend/sse-runtime-react"; import { ToggleTip } from "./ToggleTip"; import { ErrorLike } from "../../core/Atomic"; import { ErrorAlert } from "./ErrorAlert"; +import { MSErrorBoundary } from "./ErrorBoundary"; export const AppTitle = (props: { fetchable?: boolean } = {}) => { const { @@ -152,7 +153,7 @@ export const RightHeaderFloatingLogs = (props: {streamable?: boolean}) => { - + diff --git a/src/client/components/ErrorAlert.tsx b/src/client/components/ErrorAlert.tsx index 7437adad..6ca26a66 100644 --- a/src/client/components/ErrorAlert.tsx +++ b/src/client/components/ErrorAlert.tsx @@ -3,20 +3,21 @@ import { Fragment } from 'react'; import { ErrorLike } from '../../core/Atomic'; import { ChakraCodeBlock } from './CodeBlock'; import { ChakraClip } from './ChakraClipboard'; +import { ErrorIsh, isErrorIsh } from '../../core/ErrorUtils'; export interface ErrorAlertProps { - error: ErrorLike + error: ErrorIsh status?: "error" | "info" | "warning" | "success" | "neutral" } export const ErrorAlert = (props: ErrorAlertProps) => { - if(props.error === undefined || props.error === null) { + if(!isErrorIsh(props.error)) { return null; } let causes: ErrorData[] = []; - if(props.error.cause !== undefined && typeof props.error.cause === 'object' && props.error.cause !== null) { - causes = walkError(props.error.cause as ErrorLike); + if(isErrorIsh(props.error.cause)) { + causes = walkError(props.error.cause); } return ( @@ -51,7 +52,7 @@ interface ErrorData { stack?: string } -const walkError = (err: ErrorLike, errors: ErrorData[] = []): ErrorData[] => { +const walkError = (err: ErrorIsh, errors: ErrorData[] = []): ErrorData[] => { const thisErr: ErrorData = { name: err.name, code: 'code' in err ? err.code : undefined, diff --git a/src/client/components/ErrorBoundary.tsx b/src/client/components/ErrorBoundary.tsx index 4f59aacd..09d9ebe9 100644 --- a/src/client/components/ErrorBoundary.tsx +++ b/src/client/components/ErrorBoundary.tsx @@ -13,7 +13,7 @@ export const MSErrorBoundary = (props: React.PropsWithChildren) => { Error while render - + diff --git a/src/client/components/MSCollapsible.tsx b/src/client/components/MSCollapsible.tsx index 143381c3..71db76cb 100644 --- a/src/client/components/MSCollapsible.tsx +++ b/src/client/components/MSCollapsible.tsx @@ -1,6 +1,7 @@ import { Collapsible, Stack, Box, useBreakpointValue } from "@chakra-ui/react" import { ComponentProps, PropsWithChildren, useState, useEffect, useMemo } from "react"; import { LuChevronRight } from "react-icons/lu" +import { MSErrorBoundary } from "./ErrorBoundary"; //padding="0" borderWidth="0px" @@ -127,7 +128,9 @@ export const MSCollapsible = (props: MSCollapsibleProps) => { + {props.children} + diff --git a/src/client/components/PlayData.tsx b/src/client/components/PlayData.tsx index a460cd2d..3908f094 100644 --- a/src/client/components/PlayData.tsx +++ b/src/client/components/PlayData.tsx @@ -9,6 +9,7 @@ import { TextMuted } from './TextMuted.js'; import { formatNumber } from '../../core/DataUtils.js'; import { Muted } from './Typography.js'; import { ArtistCreditTags } from './ArtistCreditDisplay.js'; +import { MSErrorBoundary } from './ErrorBoundary.js'; const EmptyPlayData = () => { return ( @@ -71,19 +72,21 @@ export const PlayData = (props?: PlayInfoProps) => { return ( - {code} - - - Initial - Final - - - {codeMode ? : } - - - {codeMode ? : } - - + + {code} + + + Initial + Final + + + {codeMode ? : } + + + {codeMode ? : } + + + ); } diff --git a/src/client/components/msComponent/MSComponentDetailed.tsx b/src/client/components/msComponent/MSComponentDetailed.tsx index 65f6a860..032265ba 100644 --- a/src/client/components/msComponent/MSComponentDetailed.tsx +++ b/src/client/components/msComponent/MSComponentDetailed.tsx @@ -25,6 +25,7 @@ import dayjs from "dayjs"; import { shortTodayAwareFormat } from "../../../core/TimeUtils.js"; import { durationToHuman } from "../../../backend/utils.js"; import { tanQueries } from "../../queries/index.js"; +import { MSErrorBoundary } from "../ErrorBoundary.js"; export const MSComponentHeading = (props: { data?: Pick, fetchable?: boolean }) => { if (props.data === undefined) { @@ -111,6 +112,7 @@ export const ComponentDetailedDesktop = (props: {data?: ComponentCommonApiJson, } } return ( + @@ -135,6 +137,7 @@ export const ComponentDetailedDesktop = (props: {data?: ComponentCommonApiJson, {isComponentTypeSource(props.data.mode) ? 'Plays' : 'Scrobbles'} + ) } diff --git a/src/client/components/msComponent/MSComponentList.tsx b/src/client/components/msComponent/MSComponentList.tsx index 2e558781..59978493 100644 --- a/src/client/components/msComponent/MSComponentList.tsx +++ b/src/client/components/msComponent/MSComponentList.tsx @@ -7,6 +7,7 @@ import ky from 'ky'; import { baseUrl } from "../../utils"; import { ErrorAlert } from "../ErrorAlert"; import { tanQueries } from "../../queries"; +import { MSErrorBoundary } from "../ErrorBoundary"; export interface ComponentListProps { components: ComponentsApiJson[] @@ -33,7 +34,7 @@ export const MSComponentList = (props: ComponentListProps) => { return x.mode === 'source'; } return x.mode === 'client'; - }).map(x => props.fetchable ? : )} + }).map(x => props.fetchable ? : )} ) diff --git a/src/client/components/msComponent/MSComponentSummary.tsx b/src/client/components/msComponent/MSComponentSummary.tsx index 988901db..3ae22b5e 100644 --- a/src/client/components/msComponent/MSComponentSummary.tsx +++ b/src/client/components/msComponent/MSComponentSummary.tsx @@ -16,6 +16,7 @@ import { } from "@flamefrontend/sse-runtime-react"; import { CountLiveIndicator, DeadLetterIndicator, QueuedIndicator } from "./Stats.js"; import { ComponentStateBadge } from "../Badges.js"; +import { MSErrorBoundary } from "../ErrorBoundary.js"; const presentPlayersContainerProps: ComponentProps = { paddingTop: '2', @@ -43,7 +44,9 @@ export const MSComponentSummary = (props: { data: ComponentCommonApiJson, fetcha ); } - return ( + return ( + + @@ -67,7 +70,8 @@ export const MSComponentSummary = (props: { data: ComponentCommonApiJson, fetcha {body} - ) + + ) } const QuickStatsSource = (props: { data: ComponentCommonApiJson, streamable?: boolean }) => { diff --git a/src/core/ErrorUtils.ts b/src/core/ErrorUtils.ts index 320c5290..c8b21a98 100644 --- a/src/core/ErrorUtils.ts +++ b/src/core/ErrorUtils.ts @@ -4,6 +4,21 @@ import { ErrorLike, isErrorLike } from 'serialize-error'; export type ErrorIsh = Error | MarkOptional; +export const isErrorIsh = (val: unknown): val is ErrorIsh => { + if(val === undefined || val === null || typeof val !== 'object') { + return false; + } + if(!('message' in val)) { + return false; + } + if('cause' in val) { + if(!isErrorIsh(val.cause)) { + return false; + } + } + return true; +} + /** * Adapted from https://github.com/voxpelli/pony-cause * */ -- 2.51.2