Merge pull request #1051 from buster-so/big-nate-bus-1883-wrap-dynamic-components-in-wrapper

Big nate bus 1883 wrap dynamic components in wrapper
This commit is contained in:
Nate Kelley 2025-09-22 19:50:38 -06:00 committed by GitHub
commit 13e6c85ece
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 241 additions and 61 deletions

View File

@ -0,0 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { getAppBuildId } from '@/api/server-functions/getAppVersion';
export const useGetAppBuildId = () => {
return useQuery({
queryKey: ['app-version'] as const,
queryFn: getAppBuildId,
});
};

View File

@ -0,0 +1,8 @@
import { queryOptions } from '@tanstack/react-query';
import { getAppBuildId } from '@/api/server-functions/getAppVersion';
export const versionGetAppVersion = queryOptions({
queryKey: ['app-version'] as const,
queryFn: getAppBuildId,
refetchInterval: 20000, // 20 seconds
});

View File

@ -2,7 +2,7 @@ import { createServerFn } from '@tanstack/react-start';
export const getAppBuildId = createServerFn({ method: 'GET' }).handler(async () => {
return {
buildId: import.meta.env.VITE_BUILD_ID,
buildAt: import.meta.env.VITE_BUILD_AT,
buildId: import.meta.env.VITE_BUILD_ID as string,
buildAt: import.meta.env.VITE_BUILD_AT as string,
};
});

View File

@ -4,14 +4,19 @@ import { useEffect } from 'react';
import { Button } from '@/components/ui/buttons';
import { Card, CardContent, CardFooter } from '@/components/ui/card/CardBase';
import { Title } from '@/components/ui/typography';
import { cn } from '@/lib/utils';
import { useMount } from '../../../hooks/useMount';
export const ErrorCard = ({
footer,
className,
header = 'Looks like we hit an unexpected error',
message = "Our team has been notified via Slack. We'll take a look at the issue ASAP and get back to you.",
}: {
header?: string;
message?: string;
footer?: React.ReactNode;
className?: string;
}) => {
useMount(() => {
console.error('Error in card:', header, message);
@ -19,7 +24,10 @@ export const ErrorCard = ({
return (
<div
className=" flex h-full w-full flex-col items-center absolute inset-0 justify-center bg-linear-to-br bg-background p-8 backdrop-blur-xs backdrop-filter"
className={cn(
'flex h-full w-full flex-col items-center absolute inset-0 justify-center bg-linear-to-br bg-background p-8 backdrop-blur-xs backdrop-filter',
className
)}
role="alert"
>
<Card className="-mt-10 max-w-100">
@ -31,11 +39,13 @@ export const ErrorCard = ({
</CardContent>
<CardFooter className="w-full pt-0">
<Link to="/" className="w-full">
<Button variant="black" block size="tall">
Take me home
</Button>
</Link>
{footer || (
<Link to="/" className="w-full">
<Button variant="black" block size="tall">
Take me home
</Button>
</Link>
)}
</CardFooter>
</Card>
</div>

View File

@ -0,0 +1,129 @@
import { useNavigate } from '@tanstack/react-router';
import type React from 'react';
import { useMemo, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { Button } from '@/components/ui/buttons';
import { Paragraph, Text, Title } from '@/components/ui/typography';
import { useIsVersionChanged } from '@/context/AppVersion/useAppVersion';
import { useMount } from '@/hooks/useMount';
import { cn } from '@/lib/utils';
import { Route as HomeRoute } from '@/routes/app/_app/home';
export const LazyErrorBoundary: React.FC<React.PropsWithChildren> = ({ children }) => {
const isChanged = useIsVersionChanged();
const navigate = useNavigate();
return (
<ErrorBoundary
fallbackRender={() => {
if (isChanged) {
return (
<ComponentErrorCard
highlightType="info"
message="The app has been updated. Please reload the page."
title="New version available"
buttonText="Reload"
onButtonClick={() => {
navigate({ reloadDocument: true });
}}
/>
);
}
return (
<ComponentErrorCard
onButtonClick={() => {
navigate({ to: HomeRoute.to, reloadDocument: true });
}}
/>
);
}}
>
{children}
</ErrorBoundary>
);
};
const TestThrow = () => {
const [showThrow, setShowThrow] = useState(false);
useMount(() => {
setTimeout(() => {
setShowThrow(true);
}, 1000);
});
if (showThrow) {
throw new Error('Test error');
}
return 'loading';
};
const ComponentErrorCard: React.FC<{
title?: string;
message?: string;
containerClassName?: string;
cardClassName?: string;
buttonText?: string;
onButtonClick?: () => void;
highlightType?: 'danger' | 'info' | 'none';
}> = ({
containerClassName,
cardClassName,
highlightType = 'danger',
onButtonClick,
buttonText = 'Take me home',
title = 'We hit an unexpected error',
message = "Our team has been notified via Slack. We'll take a look at the issue ASAP and get back to you.",
}) => {
const style: React.CSSProperties = useMemo(() => {
let vars: React.CSSProperties = {};
if (highlightType === 'danger') {
vars = {
'--color-highlight-background': 'var(--color-red-100)',
'--color-highlight-border': 'red',
} as React.CSSProperties;
}
if (highlightType === 'info') {
vars = {
'--color-highlight-background': 'var(--color-purple-50)',
'--color-highlight-border': 'blue',
} as React.CSSProperties;
}
return {
...vars,
'--color-highlight-to-background': 'transparent',
'--color-highlight-to-border': 'transparent',
'--duration': '600ms',
} as React.CSSProperties;
}, [highlightType]);
return (
<div
className={cn(
'animate-highlight-fade',
'flex h-full w-full flex-col items-center justify-center p-5 bg-background text-center gap-4',
containerClassName
)}
style={style}
role="alert"
>
<div
className={cn(
'bg-background flex flex-col gap-4 p-5 rounded duration-300 shadow hover:shadow-lg transition-all border',
highlightType === 'danger' && 'shadow-red-100 ',
cardClassName
)}
>
<Title as={'h4'}>{title}</Title>
<Paragraph>{message}</Paragraph>
<Button variant="black" block size="tall" onClick={onButtonClick}>
{buttonText}
</Button>
</div>
</div>
);
};

View File

@ -1,5 +1,6 @@
import { ClientOnly } from '@tanstack/react-router';
import { lazy, Suspense } from 'react';
import { LazyErrorBoundary } from '@/components/features/global/LazyErrorBoundary';
import { PreparingYourRequestLoader } from './LoadingComponents/ChartLoadingComponents';
const BusterChartLazy = lazy(() =>
@ -7,9 +8,11 @@ const BusterChartLazy = lazy(() =>
);
export const BusterChartDynamic = (props: Parameters<typeof BusterChartLazy>[0]) => (
<Suspense fallback={<PreparingYourRequestLoader text="Loading chart..." />}>
<ClientOnly>
<BusterChartLazy {...props} />
</ClientOnly>
</Suspense>
<LazyErrorBoundary>
<Suspense fallback={<PreparingYourRequestLoader text="Loading chart..." />}>
<ClientOnly>
<BusterChartLazy {...props} />
</ClientOnly>
</Suspense>
</LazyErrorBoundary>
);

View File

@ -5,6 +5,7 @@ import type { EditorProps, OnMount } from '@monaco-editor/react';
import { ClientOnly } from '@tanstack/react-router';
import type React from 'react';
import { forwardRef, lazy, Suspense, useCallback, useMemo } from 'react';
import { LazyErrorBoundary } from '@/components/features/global/LazyErrorBoundary';
import { useMount } from '@/hooks/useMount';
import { cn } from '@/lib/utils';
import { isServer } from '@/lib/window';
@ -178,23 +179,25 @@ export const AppCodeEditor = forwardRef<AppCodeEditorHandle, AppCodeEditorProps>
)}
style={style}
>
<ClientOnly fallback={<LoadingCodeEditor />}>
<Suspense fallback={<LoadingCodeEditor />}>
<Editor
key={useDarkMode ? 'dark' : 'light'}
height={height}
language={language}
className={className}
defaultValue={defaultValue}
value={value}
theme={useDarkMode ? 'night-owl' : 'github-light'}
onMount={onMountCodeEditor}
onChange={onChangeCodeEditor}
options={memoizedMonacoEditorOptions}
loading={null}
/>
</Suspense>
</ClientOnly>
<LazyErrorBoundary>
<ClientOnly fallback={<LoadingCodeEditor />}>
<Suspense fallback={<LoadingCodeEditor />}>
<Editor
key={useDarkMode ? 'dark' : 'light'}
height={height}
language={language}
className={className}
defaultValue={defaultValue}
value={value}
theme={useDarkMode ? 'night-owl' : 'github-light'}
onMount={onMountCodeEditor}
onChange={onChangeCodeEditor}
options={memoizedMonacoEditorOptions}
loading={null}
/>
</Suspense>
</ClientOnly>
</LazyErrorBoundary>
</div>
);
}

View File

@ -1,5 +1,6 @@
import { ClientOnly } from '@tanstack/react-router';
import { lazy, Suspense } from 'react';
import { LazyErrorBoundary } from '@/components/features/global/LazyErrorBoundary';
import type { AppCodeEditorProps } from './AppCodeEditor';
import { LoadingCodeEditor } from './LoadingCodeEditor';
@ -13,10 +14,12 @@ const AppCodeEditor = lazy(() =>
export const AppCodeEditorDynamic = (props: AppCodeEditorProps) => {
return (
<ClientOnly fallback={<LoadingCodeEditor />}>
<Suspense fallback={<LoadingCodeEditor />}>
<AppCodeEditor {...props} />
</Suspense>
</ClientOnly>
<LazyErrorBoundary>
<ClientOnly fallback={<LoadingCodeEditor />}>
<Suspense fallback={<LoadingCodeEditor />}>
<AppCodeEditor {...props} />
</Suspense>
</ClientOnly>
</LazyErrorBoundary>
);
};

View File

@ -1,4 +1,5 @@
import { lazy, Suspense } from 'react';
import { LazyErrorBoundary } from '@/components/features/global/LazyErrorBoundary';
import { type UsePageReadyOptions, usePageReady } from '@/hooks/usePageReady';
import type { ReportEditorProps } from './ReportEditor';
import { ReportEditorSkeleton } from './ReportEditorSkeleton';
@ -31,9 +32,11 @@ export const DynamicReportEditor = ({ loadingOptions, ...props }: DynamicReportE
}
return (
<Suspense fallback={<ReportEditorSkeleton />}>
<DynamicReportEditorBase {...props} />
</Suspense>
<LazyErrorBoundary>
<Suspense fallback={<ReportEditorSkeleton />}>
<DynamicReportEditorBase {...props} />
</Suspense>
</LazyErrorBoundary>
);
};

View File

@ -1,20 +1,21 @@
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { getAppBuildId } from '@/api/server-functions/getAppVersion';
import { useCallback, useEffect } from 'react';
import { versionGetAppVersion } from '@/api/query_keys/version';
import { Text } from '@/components/ui/typography';
import { useWindowFocus } from '@/hooks/useWindowFocus';
import { useBusterNotifications } from '../BusterNotifications';
const browserBuild = import.meta.env.VITE_BUILD_ID;
const browserBuild = import.meta.env.VITE_BUILD_ID as string;
const checkNewVersion = (buildId: string | undefined): boolean => {
if (!buildId || !browserBuild) return false;
return buildId !== browserBuild;
};
export const useAppVersion = () => {
const { openInfoNotification } = useBusterNotifications();
const { data, refetch, isFetched } = useQuery({
queryKey: ['app-version'] as const,
queryFn: getAppBuildId,
refetchInterval: 20000, // 20 seconds
});
const isChanged = data?.buildId !== browserBuild && isFetched && browserBuild;
const { data, refetch, isFetched } = useQuery(versionGetAppVersion);
const isChanged = checkNewVersion(data?.buildId);
const reloadWindow = () => {
window.location.reload();
@ -23,7 +24,7 @@ export const useAppVersion = () => {
useWindowFocus(() => {
refetch().then(() => {
if (isChanged) {
// reloadWindow();
reloadWindow();
}
});
});
@ -48,16 +49,16 @@ export const useAppVersion = () => {
};
const AppVersionMessage = () => {
// const [countdown, setCountdown] = useState(30);
// useEffect(() => {
// const interval = setInterval(() => {
// setCountdown((prev) => Math.max(prev - 1, 0));
// if (countdown === 0) {
// // window.location.reload();
// }
// }, 1000);
// return () => clearInterval(interval);
// }, []);
// const [countdown, setCountdown] = useState(180);
// useEffect(() => {
// const interval = setInterval(() => {
// setCountdown((prev) => Math.max(prev - 1, 0));
// if (countdown === 0) {
// window.location.reload();
// }
// }, 1000);
// return () => clearInterval(interval);
// }, []);
return (
<Text>
@ -65,3 +66,11 @@ const AppVersionMessage = () => {
</Text>
);
};
export const useIsVersionChanged = () => {
const { data = false } = useQuery({
...versionGetAppVersion,
select: useCallback((data: { buildId: string }) => checkNewVersion(data.buildId), []),
});
return data;
};

View File

@ -1,6 +1,7 @@
import { ClientOnly, Outlet, useLocation, useNavigate, useSearch } from '@tanstack/react-router';
import { lazy, Suspense, useRef, useTransition } from 'react';
import { z } from 'zod';
import { LazyErrorBoundary } from '@/components/features/global/LazyErrorBoundary';
import { AppSplitter, type LayoutSize } from '@/components/ui/layouts/AppSplitter';
import { useGetMetricParams } from '@/context/Metrics/useGetMetricParams';
import { MetricViewChartController } from '@/controllers/MetricController/MetricViewChartController';
@ -89,8 +90,10 @@ const MetricEditController = lazy(() =>
const RightChildren = ({ metricId, renderChart }: { metricId: string; renderChart: boolean }) => {
return renderChart ? (
<Suspense fallback={<CircleSpinnerLoaderContainer />}>
<MetricEditController metricId={metricId} />
</Suspense>
<LazyErrorBoundary>
<Suspense fallback={<CircleSpinnerLoaderContainer />}>
<MetricEditController metricId={metricId} />
</Suspense>
</LazyErrorBoundary>
) : null;
};