Create healthcheck.tsx

This commit is contained in:
Nate Kelley 2025-09-08 16:19:19 -06:00
parent 9f17e2a140
commit 667cbfab7d
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
19 changed files with 516 additions and 73 deletions

View File

@ -8,7 +8,7 @@
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build -- --typecheck", "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build -- --typecheck",
"build:staging": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode staging", "build:staging": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode staging",
"build:production": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode production -- --typecheck", "build:production": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build --mode production -- --typecheck",
"build:local": "cross-env NODE_OPTIONS=--max-old-space-size=12288 vite build -- --typecheck --local", "build:local": "pnpm prebuild && cross-env NODE_OPTIONS=--max-old-space-size=12288 vite build -- --typecheck --local",
"build-storybook": "storybook build", "build-storybook": "storybook build",
"build:visualize": "npx vite-bundle-visualizer", "build:visualize": "npx vite-bundle-visualizer",
"deploy:dev": "pnpm run build && npx wrangler deploy .output/server/index.mjs --env dev --assets .output/public", "deploy:dev": "pnpm run build && npx wrangler deploy .output/server/index.mjs --env dev --assets .output/public",

View File

@ -69,7 +69,6 @@ export const defaultAxiosRequestHandler = async (config: InternalAxiosRequestCon
token = await getSupabaseSessionServerFn().then( token = await getSupabaseSessionServerFn().then(
({ data: { session } }) => session.access_token ({ data: { session } }) => session.access_token
); );
console.log('token', token);
} else { } else {
// Always check token validity before making requests // Always check token validity before making requests
const tokenResult = await checkTokenValidity(); const tokenResult = await checkTokenValidity();

View File

@ -11,7 +11,6 @@ export const getSupabaseSessionServerFn = createServerFn({ method: 'GET' }).hand
expires_at: session?.expires_at, expires_at: session?.expires_at,
expires_in: session?.expires_in, expires_in: session?.expires_in,
}; };
console.log('pickedSession', JSON.stringify(pickedSession));
return { return {
data: { data: {

View File

@ -4,8 +4,6 @@ import { useBusterNotifications } from '@/context/BusterNotifications';
import { signOut } from '@/integrations/supabase/signOut'; import { signOut } from '@/integrations/supabase/signOut';
import { clearAllBrowserStorage } from '@/lib/storage'; import { clearAllBrowserStorage } from '@/lib/storage';
const navigate = useNavigate();
export const useSignOut = () => { export const useSignOut = () => {
const { openErrorMessage } = useBusterNotifications(); const { openErrorMessage } = useBusterNotifications();
const navigate = useNavigate(); const navigate = useNavigate();
@ -14,19 +12,17 @@ export const useSignOut = () => {
// Then perform server-side sign out // Then perform server-side sign out
await signOut(); await signOut();
// First clear all client-side storage try {
clearAllBrowserStorage(); // First clear all client-side storage
clearAllBrowserStorage();
navigate({ to: '/auth/login' }); } catch (error) {
console.error('Error clearing browser storage', error);
// Clear all cookies
for (const cookie of document.cookie.split(';')) {
const cookieName = cookie.replace(/^ +/, '').split('=')[0];
// biome-ignore lint/suspicious/noDocumentCookie: clearing cookies
document.cookie = `${cookieName}=;expires=${new Date().toUTCString()};path=/`;
} }
} catch (error) { } catch (error) {
console.error('Error signing out', error);
openErrorMessage('Error signing out'); openErrorMessage('Error signing out');
} finally {
navigate({ to: '/auth/login' });
} }
}, [navigate, openErrorMessage]); }, [navigate, openErrorMessage]);

View File

@ -0,0 +1,24 @@
import { ErrorBoundary } from 'react-error-boundary';
import { rustErrorHandler } from '@/api/errors';
import { ErrorCard } from './GlobalErrorCard';
export const ComponentErrorCard = ({
children,
header,
message,
}: {
children: React.ReactNode;
header?: string;
message?: string;
}) => {
return (
<ErrorBoundary
fallbackRender={(e) => {
const errorMessage: string | undefined = rustErrorHandler(e).message || undefined;
return <ErrorCard header={header} message={errorMessage || message} />;
}}
>
{children}
</ErrorBoundary>
);
};

View File

@ -3,8 +3,19 @@ import { usePostHog } from 'posthog-js/react';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Button } from '@/components/ui/buttons'; import { Button } from '@/components/ui/buttons';
import { Card, CardContent, CardFooter } from '@/components/ui/card/CardBase'; import { Card, CardContent, CardFooter } from '@/components/ui/card/CardBase';
import { useMount } from '../../../hooks/useMount';
export const ErrorCard = ({
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;
}) => {
useMount(() => {
console.error('Error in card:', header, message);
});
export const ErrorCard = () => {
return ( return (
<div <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=" 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"
@ -13,11 +24,9 @@ export const ErrorCard = () => {
<Card className="-mt-10 max-w-100"> <Card className="-mt-10 max-w-100">
<CardContent> <CardContent>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<h1 className="text-2xl font-medium">Looks like we hit an unexpected error</h1> <h1 className="text-2xl font-medium">{header}</h1>
<h5 className="m-0 text-base font-medium text-gray-600"> <h5 className="m-0 text-base font-medium text-gray-600">{message}</h5>
{`Our team has been notified via Slack. We'll take a look at the issue ASAP and get back to you.`}
</h5>
</div> </div>
</CardContent> </CardContent>
@ -42,8 +51,6 @@ export const GlobalErrorCard: ErrorRouteComponent = ({ error }) => {
if (isPosthogLoaded) { if (isPosthogLoaded) {
posthog.captureException(error); posthog.captureException(error);
} }
console.error('GlobalErrorCard error', error);
}, [error]); }, [error]);
return <ErrorCard />; return <ErrorCard />;

View File

@ -6,6 +6,7 @@ import CircleUser from '@/components/ui/icons/NucleoIconOutlined/circle-user';
import { createSidebarGroup } from '@/components/ui/sidebar/create-sidebar-item'; import { createSidebarGroup } from '@/components/ui/sidebar/create-sidebar-item';
import LockCircle from '../../ui/icons/NucleoIconOutlined/lock-circle'; import LockCircle from '../../ui/icons/NucleoIconOutlined/lock-circle';
import { type ISidebarGroup, Sidebar } from '../../ui/sidebar'; import { type ISidebarGroup, Sidebar } from '../../ui/sidebar';
import { ComponentErrorCard } from '../global/ComponentErrorCard';
import { SidebarUserFooter } from './SidebarUserFooter'; import { SidebarUserFooter } from './SidebarUserFooter';
const accountItems: ISidebarGroup = createSidebarGroup({ const accountItems: ISidebarGroup = createSidebarGroup({
@ -107,12 +108,14 @@ export const SidebarSettings = () => {
}, [isAdmin]); }, [isAdmin]);
return ( return (
<Sidebar <ComponentErrorCard header="Settings Sidebar">
content={sidebarItems} <Sidebar
header={useMemo(() => <SidebarSettingsHeader />, [])} content={sidebarItems}
footer={useMemo(() => <SidebarUserFooter />, [])} header={useMemo(() => <SidebarSettingsHeader />, [])}
useCollapsible={isUserRegistered} footer={useMemo(() => <SidebarUserFooter />, [])}
/> useCollapsible={isUserRegistered}
/>
</ComponentErrorCard>
); );
}; };

View File

@ -210,7 +210,6 @@ const InlineComboboxInput = React.forwardRef<
})?.width + 8 })?.width + 8
); );
}, [placeholder]); }, [placeholder]);
console.log(placeHolderWidth);
/** /**
* To create an auto-resizing input, we render a visually hidden span * To create an auto-resizing input, we render a visually hidden span

View File

@ -7,6 +7,7 @@ import {
useGetUserBasicInfo, useGetUserBasicInfo,
useGetUserOrganization, useGetUserOrganization,
} from '@/api/buster_rest/users/useGetUserInfo'; } from '@/api/buster_rest/users/useGetUserInfo';
import { ComponentErrorCard } from '@/components/features/global/ComponentErrorCard';
import { isDev } from '@/config/dev'; import { isDev } from '@/config/dev';
import { env } from '@/env'; import { env } from '@/env';
import packageJson from '../../../package.json'; import packageJson from '../../../package.json';
@ -16,16 +17,18 @@ const POSTHOG_KEY = env.VITE_PUBLIC_POSTHOG_KEY;
const DEBUG_POSTHOG = false; const DEBUG_POSTHOG = false;
export const BusterPosthogProvider: React.FC<PropsWithChildren> = ({ children }) => { export const BusterPosthogProvider: React.FC<PropsWithChildren> = ({ children }) => {
console.log('POSTHOG_KEY', POSTHOG_KEY);
console.log('DEBUG_POSTHOG', DEBUG_POSTHOG);
if ((isDev && !DEBUG_POSTHOG) || !POSTHOG_KEY) { if ((isDev && !DEBUG_POSTHOG) || !POSTHOG_KEY) {
return <>{children}</>; return <>{children}</>;
} }
console.log('POSTHOG_KEY22', POSTHOG_KEY); return (
console.log('DEBUG_POSTHOG22', DEBUG_POSTHOG); <ComponentErrorCard
header="Posthog failed to load"
return <PosthogWrapper>{children}</PosthogWrapper>; message="Our team has been notified via Slack. We'll take a look at the issue ASAP and get back to you."
>
<PosthogWrapper>{children}</PosthogWrapper>
</ComponentErrorCard>
);
}; };
BusterPosthogProvider.displayName = 'BusterPosthogProvider'; BusterPosthogProvider.displayName = 'BusterPosthogProvider';
@ -49,7 +52,6 @@ const options: Partial<PostHogConfig> = {
}; };
const PosthogWrapper: React.FC<PropsWithChildren> = ({ children }) => { const PosthogWrapper: React.FC<PropsWithChildren> = ({ children }) => {
console.log('PosthogWrapper');
const user = useGetUserBasicInfo(); const user = useGetUserBasicInfo();
const { data: userTeams } = useGetUserTeams({ userId: user?.id ?? '' }); const { data: userTeams } = useGetUserTeams({ userId: user?.id ?? '' });
const userOrganizations = useGetUserOrganization(); const userOrganizations = useGetUserOrganization();
@ -113,8 +115,6 @@ const PosthogWrapper: React.FC<PropsWithChildren> = ({ children }) => {
return <>{children}</>; return <>{children}</>;
} }
console.log('PostHogProvider!!!!', PostHogProvider);
return ( return (
<ClientOnly> <ClientOnly>
<PostHogProvider client={posthogModules.posthog}>{children}</PostHogProvider> <PostHogProvider client={posthogModules.posthog}>{children}</PostHogProvider>

View File

@ -17,8 +17,6 @@ export const AppProviders: React.FC<PropsWithChildren<SupabaseContextType>> = ({
user, user,
accessToken, accessToken,
}) => { }) => {
console.log('user11', user);
console.log('accessToken11', accessToken);
return ( return (
<SupabaseContextProvider user={user} accessToken={accessToken}> <SupabaseContextProvider user={user} accessToken={accessToken}>
<BusterPosthogProvider>{children}</BusterPosthogProvider> <BusterPosthogProvider>{children}</BusterPosthogProvider>

View File

@ -66,10 +66,7 @@ export const SupabaseContext = createContext<ReturnType<typeof useSupabaseContex
export const SupabaseContextProvider: React.FC< export const SupabaseContextProvider: React.FC<
SupabaseContextType & { children: React.ReactNode } SupabaseContextType & { children: React.ReactNode }
> = React.memo(({ user, accessToken, children }) => { > = React.memo(({ user, accessToken, children }) => {
console.log('user22', user);
console.log('accessToken22', accessToken);
const value = useSupabaseContextInternal({ user, accessToken }); const value = useSupabaseContextInternal({ user, accessToken });
console.log('value22', value);
return <SupabaseContext.Provider value={value}>{children}</SupabaseContext.Provider>; return <SupabaseContext.Provider value={value}>{children}</SupabaseContext.Provider>;
}); });

View File

@ -13,8 +13,6 @@ const isValidRedirectUrl = (url: string): boolean => {
} }
}; };
const fallbackCallbackUrl = `${env.VITE_PUBLIC_URL}/auth/callback`;
// Common OAuth handler to reduce code duplication // Common OAuth handler to reduce code duplication
const handleOAuthSignIn = async ( const handleOAuthSignIn = async (
provider: 'google' | 'github' | 'azure', provider: 'google' | 'github' | 'azure',
@ -55,6 +53,7 @@ export const signInWithEmailAndPassword = createServerFn({ method: 'POST' })
) )
.handler(async ({ data }) => { .handler(async ({ data }) => {
const supabase = getSupabaseServerClient(); const supabase = getSupabaseServerClient();
const { error } = await supabase.auth.signInWithPassword({ const { error } = await supabase.auth.signInWithPassword({
email: data.email, email: data.email,
password: data.password, password: data.password,

View File

@ -1,4 +1,5 @@
import type React from 'react'; import type React from 'react';
import { ComponentErrorCard } from '@/components/features/global/ComponentErrorCard';
import { SidebarSettings } from '@/components/features/sidebars/SidebarSettings'; import { SidebarSettings } from '@/components/features/sidebars/SidebarSettings';
import { AppLayout, type LayoutSize } from '@/components/ui/layouts/AppLayout'; import { AppLayout, type LayoutSize } from '@/components/ui/layouts/AppLayout';
@ -16,13 +17,15 @@ export const SettingsAppLayout: React.FC<IPrimaryAppLayoutProps> = ({
defaultLayout, defaultLayout,
}) => { }) => {
return ( return (
<AppLayout <ComponentErrorCard header="Settings App Layout">
autoSaveId={layoutId} <AppLayout
defaultLayout={defaultLayout} autoSaveId={layoutId}
initialLayout={initialLayout} defaultLayout={defaultLayout}
sidebar={<SidebarSettings />} initialLayout={initialLayout}
> sidebar={<SidebarSettings />}
{children} >
</AppLayout> {children}
</AppLayout>
</ComponentErrorCard>
); );
}; };

View File

@ -20,6 +20,7 @@ import { Route as AppIndexRouteImport } from './routes/app/index'
import { Route as AuthResetPasswordRouteImport } from './routes/auth.reset-password' import { Route as AuthResetPasswordRouteImport } from './routes/auth.reset-password'
import { Route as AuthLogoutRouteImport } from './routes/auth.logout' import { Route as AuthLogoutRouteImport } from './routes/auth.logout'
import { Route as AuthLoginRouteImport } from './routes/auth.login' import { Route as AuthLoginRouteImport } from './routes/auth.login'
import { Route as AppHealthcheckRouteImport } from './routes/app/healthcheck'
import { Route as AppSettingsRouteImport } from './routes/app/_settings' import { Route as AppSettingsRouteImport } from './routes/app/_settings'
import { Route as AppAppRouteImport } from './routes/app/_app' import { Route as AppAppRouteImport } from './routes/app/_app'
import { Route as EmbedReportReportIdRouteImport } from './routes/embed.report.$reportId' import { Route as EmbedReportReportIdRouteImport } from './routes/embed.report.$reportId'
@ -29,6 +30,7 @@ import { Route as AppSettingsRestricted_layoutRouteImport } from './routes/app/_
import { Route as AppSettingsPermissionsRouteImport } from './routes/app/_settings/_permissions' import { Route as AppSettingsPermissionsRouteImport } from './routes/app/_settings/_permissions'
import { Route as AppAppHomeRouteImport } from './routes/app/_app/home' import { Route as AppAppHomeRouteImport } from './routes/app/_app/home'
import { Route as AppAppAssetRouteImport } from './routes/app/_app/_asset' import { Route as AppAppAssetRouteImport } from './routes/app/_app/_asset'
import { Route as AppSettingsSettingsIndexRouteImport } from './routes/app/_settings/settings.index'
import { Route as AppAppReportsIndexRouteImport } from './routes/app/_app/reports.index' import { Route as AppAppReportsIndexRouteImport } from './routes/app/_app/reports.index'
import { Route as AppAppMetricsIndexRouteImport } from './routes/app/_app/metrics.index' import { Route as AppAppMetricsIndexRouteImport } from './routes/app/_app/metrics.index'
import { Route as AppAppLogsIndexRouteImport } from './routes/app/_app/logs.index' import { Route as AppAppLogsIndexRouteImport } from './routes/app/_app/logs.index'
@ -198,6 +200,11 @@ const AuthLoginRoute = AuthLoginRouteImport.update({
path: '/login', path: '/login',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
const AppHealthcheckRoute = AppHealthcheckRouteImport.update({
id: '/healthcheck',
path: '/healthcheck',
getParentRoute: () => AppRoute,
} as any)
const AppSettingsRoute = AppSettingsRouteImport.update({ const AppSettingsRoute = AppSettingsRouteImport.update({
id: '/_settings', id: '/_settings',
getParentRoute: () => AppRoute, getParentRoute: () => AppRoute,
@ -240,6 +247,12 @@ const AppAppAssetRoute = AppAppAssetRouteImport.update({
id: '/_asset', id: '/_asset',
getParentRoute: () => AppAppRoute, getParentRoute: () => AppAppRoute,
} as any) } as any)
const AppSettingsSettingsIndexRoute =
AppSettingsSettingsIndexRouteImport.update({
id: '/settings/',
path: '/settings/',
getParentRoute: () => AppSettingsRoute,
} as any)
const AppAppReportsIndexRoute = AppAppReportsIndexRouteImport.update({ const AppAppReportsIndexRoute = AppAppReportsIndexRouteImport.update({
id: '/reports/', id: '/reports/',
path: '/reports/', path: '/reports/',
@ -913,6 +926,7 @@ export interface FileRoutesByFullPath {
'/app': typeof AppSettingsRestricted_layoutAdmin_onlyRouteWithChildren '/app': typeof AppSettingsRestricted_layoutAdmin_onlyRouteWithChildren
'/auth': typeof AuthRouteWithChildren '/auth': typeof AuthRouteWithChildren
'/healthcheck': typeof HealthcheckRoute '/healthcheck': typeof HealthcheckRoute
'/app/healthcheck': typeof AppHealthcheckRoute
'/auth/login': typeof AuthLoginRoute '/auth/login': typeof AuthLoginRoute
'/auth/logout': typeof AuthLogoutRoute '/auth/logout': typeof AuthLogoutRoute
'/auth/reset-password': typeof AuthResetPasswordRoute '/auth/reset-password': typeof AuthResetPasswordRoute
@ -929,6 +943,7 @@ export interface FileRoutesByFullPath {
'/app/logs': typeof AppAppLogsIndexRoute '/app/logs': typeof AppAppLogsIndexRoute
'/app/metrics': typeof AppAppMetricsIndexRoute '/app/metrics': typeof AppAppMetricsIndexRoute
'/app/reports': typeof AppAppReportsIndexRoute '/app/reports': typeof AppAppReportsIndexRoute
'/app/settings': typeof AppSettingsSettingsIndexRoute
'/app/chats/$chatId': typeof AppAppAssetChatsChatIdRouteWithChildren '/app/chats/$chatId': typeof AppAppAssetChatsChatIdRouteWithChildren
'/app/datasets/$datasetId/editor': typeof AppAppDatasetsDatasetIdEditorRoute '/app/datasets/$datasetId/editor': typeof AppAppDatasetsDatasetIdEditorRoute
'/app/datasets/$datasetId/overview': typeof AppAppDatasetsDatasetIdOverviewRoute '/app/datasets/$datasetId/overview': typeof AppAppDatasetsDatasetIdOverviewRoute
@ -1018,6 +1033,7 @@ export interface FileRoutesByTo {
'/auth': typeof AuthRouteWithChildren '/auth': typeof AuthRouteWithChildren
'/healthcheck': typeof HealthcheckRoute '/healthcheck': typeof HealthcheckRoute
'/app': typeof AppSettingsRestricted_layoutAdmin_onlyRouteWithChildren '/app': typeof AppSettingsRestricted_layoutAdmin_onlyRouteWithChildren
'/app/healthcheck': typeof AppHealthcheckRoute
'/auth/login': typeof AuthLoginRoute '/auth/login': typeof AuthLoginRoute
'/auth/logout': typeof AuthLogoutRoute '/auth/logout': typeof AuthLogoutRoute
'/auth/reset-password': typeof AuthResetPasswordRoute '/auth/reset-password': typeof AuthResetPasswordRoute
@ -1033,6 +1049,7 @@ export interface FileRoutesByTo {
'/app/logs': typeof AppAppLogsIndexRoute '/app/logs': typeof AppAppLogsIndexRoute
'/app/metrics': typeof AppAppMetricsIndexRoute '/app/metrics': typeof AppAppMetricsIndexRoute
'/app/reports': typeof AppAppReportsIndexRoute '/app/reports': typeof AppAppReportsIndexRoute
'/app/settings': typeof AppSettingsSettingsIndexRoute
'/app/datasets/$datasetId/editor': typeof AppAppDatasetsDatasetIdEditorRoute '/app/datasets/$datasetId/editor': typeof AppAppDatasetsDatasetIdEditorRoute
'/app/datasets/$datasetId/overview': typeof AppAppDatasetsDatasetIdOverviewRoute '/app/datasets/$datasetId/overview': typeof AppAppDatasetsDatasetIdOverviewRoute
'/app/settings/profile': typeof AppSettingsRestricted_layoutSettingsProfileRoute '/app/settings/profile': typeof AppSettingsRestricted_layoutSettingsProfileRoute
@ -1108,6 +1125,7 @@ export interface FileRoutesById {
'/healthcheck': typeof HealthcheckRoute '/healthcheck': typeof HealthcheckRoute
'/app/_app': typeof AppAppRouteWithChildren '/app/_app': typeof AppAppRouteWithChildren
'/app/_settings': typeof AppSettingsRouteWithChildren '/app/_settings': typeof AppSettingsRouteWithChildren
'/app/healthcheck': typeof AppHealthcheckRoute
'/auth/login': typeof AuthLoginRoute '/auth/login': typeof AuthLoginRoute
'/auth/logout': typeof AuthLogoutRoute '/auth/logout': typeof AuthLogoutRoute
'/auth/reset-password': typeof AuthResetPasswordRoute '/auth/reset-password': typeof AuthResetPasswordRoute
@ -1128,6 +1146,7 @@ export interface FileRoutesById {
'/app/_app/logs/': typeof AppAppLogsIndexRoute '/app/_app/logs/': typeof AppAppLogsIndexRoute
'/app/_app/metrics/': typeof AppAppMetricsIndexRoute '/app/_app/metrics/': typeof AppAppMetricsIndexRoute
'/app/_app/reports/': typeof AppAppReportsIndexRoute '/app/_app/reports/': typeof AppAppReportsIndexRoute
'/app/_settings/settings/': typeof AppSettingsSettingsIndexRoute
'/app/_app/_asset/chats/$chatId': typeof AppAppAssetChatsChatIdRouteWithChildren '/app/_app/_asset/chats/$chatId': typeof AppAppAssetChatsChatIdRouteWithChildren
'/app/_app/datasets/$datasetId/editor': typeof AppAppDatasetsDatasetIdEditorRoute '/app/_app/datasets/$datasetId/editor': typeof AppAppDatasetsDatasetIdEditorRoute
'/app/_app/datasets/$datasetId/overview': typeof AppAppDatasetsDatasetIdOverviewRoute '/app/_app/datasets/$datasetId/overview': typeof AppAppDatasetsDatasetIdOverviewRoute
@ -1229,6 +1248,7 @@ export interface FileRouteTypes {
| '/app' | '/app'
| '/auth' | '/auth'
| '/healthcheck' | '/healthcheck'
| '/app/healthcheck'
| '/auth/login' | '/auth/login'
| '/auth/logout' | '/auth/logout'
| '/auth/reset-password' | '/auth/reset-password'
@ -1245,6 +1265,7 @@ export interface FileRouteTypes {
| '/app/logs' | '/app/logs'
| '/app/metrics' | '/app/metrics'
| '/app/reports' | '/app/reports'
| '/app/settings'
| '/app/chats/$chatId' | '/app/chats/$chatId'
| '/app/datasets/$datasetId/editor' | '/app/datasets/$datasetId/editor'
| '/app/datasets/$datasetId/overview' | '/app/datasets/$datasetId/overview'
@ -1334,6 +1355,7 @@ export interface FileRouteTypes {
| '/auth' | '/auth'
| '/healthcheck' | '/healthcheck'
| '/app' | '/app'
| '/app/healthcheck'
| '/auth/login' | '/auth/login'
| '/auth/logout' | '/auth/logout'
| '/auth/reset-password' | '/auth/reset-password'
@ -1349,6 +1371,7 @@ export interface FileRouteTypes {
| '/app/logs' | '/app/logs'
| '/app/metrics' | '/app/metrics'
| '/app/reports' | '/app/reports'
| '/app/settings'
| '/app/datasets/$datasetId/editor' | '/app/datasets/$datasetId/editor'
| '/app/datasets/$datasetId/overview' | '/app/datasets/$datasetId/overview'
| '/app/settings/profile' | '/app/settings/profile'
@ -1423,6 +1446,7 @@ export interface FileRouteTypes {
| '/healthcheck' | '/healthcheck'
| '/app/_app' | '/app/_app'
| '/app/_settings' | '/app/_settings'
| '/app/healthcheck'
| '/auth/login' | '/auth/login'
| '/auth/logout' | '/auth/logout'
| '/auth/reset-password' | '/auth/reset-password'
@ -1443,6 +1467,7 @@ export interface FileRouteTypes {
| '/app/_app/logs/' | '/app/_app/logs/'
| '/app/_app/metrics/' | '/app/_app/metrics/'
| '/app/_app/reports/' | '/app/_app/reports/'
| '/app/_settings/settings/'
| '/app/_app/_asset/chats/$chatId' | '/app/_app/_asset/chats/$chatId'
| '/app/_app/datasets/$datasetId/editor' | '/app/_app/datasets/$datasetId/editor'
| '/app/_app/datasets/$datasetId/overview' | '/app/_app/datasets/$datasetId/overview'
@ -1627,6 +1652,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthLoginRouteImport preLoaderRoute: typeof AuthLoginRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
'/app/healthcheck': {
id: '/app/healthcheck'
path: '/healthcheck'
fullPath: '/app/healthcheck'
preLoaderRoute: typeof AppHealthcheckRouteImport
parentRoute: typeof AppRoute
}
'/app/_settings': { '/app/_settings': {
id: '/app/_settings' id: '/app/_settings'
path: '' path: ''
@ -1690,6 +1722,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppAppAssetRouteImport preLoaderRoute: typeof AppAppAssetRouteImport
parentRoute: typeof AppAppRoute parentRoute: typeof AppAppRoute
} }
'/app/_settings/settings/': {
id: '/app/_settings/settings/'
path: '/settings'
fullPath: '/app/settings'
preLoaderRoute: typeof AppSettingsSettingsIndexRouteImport
parentRoute: typeof AppSettingsRoute
}
'/app/_app/reports/': { '/app/_app/reports/': {
id: '/app/_app/reports/' id: '/app/_app/reports/'
path: '/reports' path: '/reports'
@ -3120,12 +3159,14 @@ const AppSettingsRestricted_layoutRouteWithChildren =
interface AppSettingsRouteChildren { interface AppSettingsRouteChildren {
AppSettingsPermissionsRoute: typeof AppSettingsPermissionsRouteWithChildren AppSettingsPermissionsRoute: typeof AppSettingsPermissionsRouteWithChildren
AppSettingsRestricted_layoutRoute: typeof AppSettingsRestricted_layoutRouteWithChildren AppSettingsRestricted_layoutRoute: typeof AppSettingsRestricted_layoutRouteWithChildren
AppSettingsSettingsIndexRoute: typeof AppSettingsSettingsIndexRoute
} }
const AppSettingsRouteChildren: AppSettingsRouteChildren = { const AppSettingsRouteChildren: AppSettingsRouteChildren = {
AppSettingsPermissionsRoute: AppSettingsPermissionsRouteWithChildren, AppSettingsPermissionsRoute: AppSettingsPermissionsRouteWithChildren,
AppSettingsRestricted_layoutRoute: AppSettingsRestricted_layoutRoute:
AppSettingsRestricted_layoutRouteWithChildren, AppSettingsRestricted_layoutRouteWithChildren,
AppSettingsSettingsIndexRoute: AppSettingsSettingsIndexRoute,
} }
const AppSettingsRouteWithChildren = AppSettingsRoute._addFileChildren( const AppSettingsRouteWithChildren = AppSettingsRoute._addFileChildren(
@ -3135,12 +3176,14 @@ const AppSettingsRouteWithChildren = AppSettingsRoute._addFileChildren(
interface AppRouteChildren { interface AppRouteChildren {
AppAppRoute: typeof AppAppRouteWithChildren AppAppRoute: typeof AppAppRouteWithChildren
AppSettingsRoute: typeof AppSettingsRouteWithChildren AppSettingsRoute: typeof AppSettingsRouteWithChildren
AppHealthcheckRoute: typeof AppHealthcheckRoute
AppIndexRoute: typeof AppIndexRoute AppIndexRoute: typeof AppIndexRoute
} }
const AppRouteChildren: AppRouteChildren = { const AppRouteChildren: AppRouteChildren = {
AppAppRoute: AppAppRouteWithChildren, AppAppRoute: AppAppRouteWithChildren,
AppSettingsRoute: AppSettingsRouteWithChildren, AppSettingsRoute: AppSettingsRouteWithChildren,
AppHealthcheckRoute: AppHealthcheckRoute,
AppIndexRoute: AppIndexRoute, AppIndexRoute: AppIndexRoute,
} }

View File

@ -28,22 +28,14 @@ export const Route = createFileRoute('/app')({
loader: async ({ context }) => { loader: async ({ context }) => {
const { queryClient, accessToken } = context; const { queryClient, accessToken } = context;
try { try {
const [initialLayout, user, userInfo, userFavorites, listDatasources, datasets] = const [initialLayout, user] = await Promise.all([
await Promise.all([ getAppLayout({ id: PRIMARY_APP_LAYOUT_ID }),
getAppLayout({ id: PRIMARY_APP_LAYOUT_ID }), getSupabaseUser(),
getSupabaseUser(), prefetchGetMyUserInfo(queryClient),
prefetchGetMyUserInfo(queryClient), prefetchGetUserFavorites(queryClient),
prefetchGetUserFavorites(queryClient), prefetchListDatasources(queryClient),
prefetchListDatasources(queryClient), prefetchGetDatasets(queryClient),
prefetchGetDatasets(queryClient), ]);
]);
console.log('initialLayout', initialLayout);
console.log('user', user);
console.log('userInfo', userInfo);
console.log('userFavorites', userFavorites);
console.log('listDatasources', listDatasources);
console.log('datasets', datasets);
if (!user) { if (!user) {
throw redirect({ to: '/auth/login' }); throw redirect({ to: '/auth/login' });
@ -63,8 +55,6 @@ export const Route = createFileRoute('/app')({
}, },
component: () => { component: () => {
const { user, accessToken } = Route.useLoaderData(); const { user, accessToken } = Route.useLoaderData();
console.log('user', user);
console.log('accessToken', accessToken);
return ( return (
<AppProviders user={user} accessToken={accessToken}> <AppProviders user={user} accessToken={accessToken}>

View File

@ -6,6 +6,7 @@ const routeApi = getRouteApi('/app');
export const Route = createFileRoute('/app/_settings')({ export const Route = createFileRoute('/app/_settings')({
component: () => { component: () => {
const { initialLayout, layoutId, defaultLayout } = routeApi.useLoaderData(); const { initialLayout, layoutId, defaultLayout } = routeApi.useLoaderData();
return ( return (
<SettingsAppLayout <SettingsAppLayout
initialLayout={initialLayout} initialLayout={initialLayout}

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/app/_settings/settings/')({
component: RouteComponent,
});
function RouteComponent() {
return <div>Hello "/app/_settings/"!</div>;
}

View File

@ -0,0 +1,375 @@
import type { HealthCheckResponse } from '@buster/server-shared/healthcheck';
import { createFileRoute } from '@tanstack/react-router';
import { useHealthcheck } from '@/api/buster_rest/healthcheck/queryRequests';
import { useGetUserBasicInfo } from '@/api/buster_rest/users/useGetUserInfo';
import type { RustApiError } from '@/api/errors';
import { useGetSupabaseUser } from '@/context/Supabase';
export const Route = createFileRoute('/app/healthcheck')({
component: RouteComponent,
});
function RouteComponent() {
const { data, isLoading, error } = useHealthcheck();
const supabaseUser = useGetSupabaseUser();
const user = useGetUserBasicInfo();
if (isLoading) {
return <LoadingState />;
}
if (error) {
return <ErrorState error={error} />;
}
if (!data) {
return <ErrorState error={new Error('No data received')} />;
}
return <HealthcheckDashboard data={data} supabaseUser={supabaseUser} user={user} />;
}
function LoadingState() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Checking system health...</p>
</div>
</div>
);
}
function ErrorState({ error }: { error: Error | RustApiError }) {
return (
<div className="min-h-screen bg-gradient-to-br from-red-50 to-white flex items-center justify-center">
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full mx-4">
<div className="flex items-center mb-4">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mr-4">
<svg
className="w-6 h-6 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
</div>
<div>
<h2 className="text-xl font-semibold text-gray-900">Health Check Failed</h2>
<p className="text-gray-600">Unable to retrieve system status</p>
</div>
</div>
<div className="bg-red-50 border border-red-200 rounded-md p-3">
<p className="text-sm text-red-800">{error.message}</p>
</div>
</div>
</div>
);
}
function HealthcheckDashboard({
data,
supabaseUser,
user,
}: {
data: HealthCheckResponse;
supabaseUser: ReturnType<typeof useGetSupabaseUser>;
user: ReturnType<typeof useGetUserBasicInfo>;
}) {
const getStatusColor = (status: string) => {
switch (status) {
case 'healthy':
case 'pass':
return 'text-green-600 bg-green-100';
case 'degraded':
case 'warn':
return 'text-yellow-600 bg-yellow-100';
case 'unhealthy':
case 'fail':
return 'text-red-600 bg-red-100';
default:
return 'text-gray-600 bg-gray-100';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'healthy':
case 'pass':
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
);
case 'degraded':
case 'warn':
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
);
case 'unhealthy':
case 'fail':
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
default:
return (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
}
};
const formatUptime = (seconds: number) => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days}d ${hours}h ${minutes}m`;
}
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
};
const formatTimestamp = (timestamp: string) => {
return new Date(timestamp).toLocaleString();
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-white p-6">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">System Health Dashboard</h1>
<p className="text-gray-600">Real-time monitoring of system components</p>
</div>
{/* User Data Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Supabase User Data */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-4 h-4 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
</div>
Supabase User
</h3>
<div className="bg-gray-50 rounded-md p-4">
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
{JSON.stringify(supabaseUser, null, 2)}
</pre>
</div>
</div>
{/* User Basic Info */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
<div className="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-4 h-4 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
User Basic Info
</h3>
<div className="bg-gray-50 rounded-md p-4">
<pre className="text-sm text-gray-800 whitespace-pre-wrap overflow-auto">
{JSON.stringify(user, null, 2)}
</pre>
</div>
</div>
</div>
{/* Overall Status Card */}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div
className={`w-12 h-12 rounded-full flex items-center justify-center mr-4 ${getStatusColor(
data.status
)}`}
>
{getStatusIcon(data.status)}
</div>
<div>
<h2 className="text-2xl font-semibold text-gray-900 capitalize">{data.status}</h2>
<p className="text-gray-600">Overall System Status</p>
</div>
</div>
<div className="text-right">
<p className="text-sm text-gray-500">Last checked</p>
<p className="text-gray-900 font-medium">{formatTimestamp(data.timestamp)}</p>
</div>
</div>
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-5 h-5 text-blue-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<div>
<p className="text-sm text-gray-500">Uptime</p>
<p className="text-xl font-semibold text-gray-900">{formatUptime(data.uptime)}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-5 h-5 text-purple-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h4a1 1 0 011 1v1a1 1 0 01-1 1h-1v12a2 2 0 01-2 2H6a2 2 0 01-2-2V7H3a1 1 0 01-1-1V5a1 1 0 011-1h4z"
/>
</svg>
</div>
<div>
<p className="text-sm text-gray-500">Version</p>
<p className="text-xl font-semibold text-gray-900">{data.version}</p>
</div>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center mr-3">
<svg
className="w-5 h-5 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9v-9m0-9v9"
/>
</svg>
</div>
<div>
<p className="text-sm text-gray-500">Environment</p>
<p className="text-xl font-semibold text-gray-900 capitalize">{data.environment}</p>
</div>
</div>
</div>
</div>
{/* Component Checks */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-semibold text-gray-900 mb-6">Component Health Checks</h3>
<div className="space-y-4">
{Object.entries(data.checks).map(([componentName, check]) => (
<div key={componentName} className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center mr-3 ${getStatusColor(
check.status
)}`}
>
{getStatusIcon(check.status)}
</div>
<div>
<h4 className="font-medium text-gray-900 capitalize">{componentName}</h4>
{check.message && <p className="text-sm text-gray-600">{check.message}</p>}
</div>
</div>
<div className="text-right">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${getStatusColor(
check.status
)}`}
>
{check.status}
</span>
{check.responseTime && (
<p className="text-sm text-gray-500 mt-1">{check.responseTime}ms</p>
)}
</div>
</div>
</div>
))}
</div>
</div>
{/* Footer */}
<div className="mt-8 text-center text-gray-500 text-sm">
<p>System health is monitored continuously. Data refreshes automatically.</p>
</div>
</div>
</div>
);
}

View File

@ -10,7 +10,8 @@ const config = defineConfig(({ command, mode }) => {
const isProduction = mode === 'production' || mode === 'staging'; const isProduction = mode === 'production' || mode === 'staging';
const isTypecheck = process.argv.includes('--typecheck') || process.env.TYPECHECK === 'true'; const isTypecheck = process.argv.includes('--typecheck') || process.env.TYPECHECK === 'true';
const useChecker = !process.env.VITEST && isBuild; const useChecker = !process.env.VITEST && isBuild;
const isLocalBuild = process.argv.includes('--local'); const isLocalBuild = process.argv.includes('--local') || mode === 'development';
const target = isLocalBuild ? ('bun' as const) : ('cloudflare-module' as const);
return { return {
server: { port: 3000 }, server: { port: 3000 },
@ -20,7 +21,7 @@ const config = defineConfig(({ command, mode }) => {
tailwindcss(), tailwindcss(),
tanstackStart({ tanstackStart({
customViteReactPlugin: true, customViteReactPlugin: true,
target: isLocalBuild ? 'bun' : 'cloudflare-module', target,
}), }),
viteReact(), viteReact(),
useChecker useChecker